CrewCTF - OhPHP
Background
-
PHP - substr()
:::spoiler
substr( $string , $start , $length )
$strting
是原始的字串,$start
是要開始擷取的位置,$length
則為要截取的字串長度,要注要的是$start
與 $length 都必須為數字才有作用,可以是正整數,也可以是負整數,以下提供幾個範例參考。 ::: -
PHP - strstr()
:::spoiler
查找 “world” 在 “Hello world!” 中是否存在,如果是,返回該字符串及後面剩餘部分
1
2
3<?php echo strstr("Hello world!","world"); // 輸出 world! ?>
:::
-
PHP - strrev()
:::spoiler
反轉字符串 “Hello World!”:
1
2
3<?php echo strrev("Hello world!"); // 輸出!dlroW olleH ?>
:::
-
PHP strnatcmp()
:::spoiler
使用”自然”算法來比較兩個字符串(區分大小寫):
1
2
3
4
5<?php echo strnatcmp("2Hello world!","10Hello world!"); // 輸出-1 echo "<br>"; echo strnatcmp("10Hello world!","2Hello world!"); // 輸出+1 ?>
strnatcmp() 函數使用一種”自然”算法來比較兩個字符串。 在自然算法中,數字 2 小於數字 10。在計算機排序中,10 小於 2,這是因為 10 中的第一個數字小於 2。 :::
-
PHP - crc32()
:::spoiler
1
2
3
4<?php $str = crc32("Hello World!"); // Output: 472456355 printf("%u\n",$str); ?>
:::
- PHP - srand()
-
PHP - strpos()
:::spoiler
查找 “php” 在字符串中第一次出現的位置:
1
2
3<?php echo strpos("You love php, I love php too!","php"); // Output: 9 ?>
:::
-
PHP - array_sum()
:::spoiler
array_sum 這個函式用來統計陣列 Array 中的數值總數,並回傳統計值,如果陣列內的數值為整數,array_sum 傳回統計值將為整數,若陣列內數值為浮點數,則 array_sum 可能會傳回整數或浮點數。
1
2
3
4
5
6<?php $a = array(2, 3, 4); $b = array("a" => 1.2, "b" => 2.0, "c" => 3.3); echo array_sum($a); // Output: 9 echo array_sum($b); // Output: 6.5 ?>
:::
-
PHP - pack()
:::spoiler
pack() 函數把數據裝入一個二進制字符串。詳細的格式可以看原網頁
1
2
3<?php echo pack("C3",80,72,80); ?>
:::
- PHP - Arrays
-
PHP使用SHA256、SHA512等演算法的寫法
:::spoiler
1
2
3
4
5
6
7<?php echo hash('sha256', 'abc'); echo hash('sha512', 'abc'); // md5, sha1.. 等等也都可以用此寫法 echo hash('md5', 'abc'); echo hash('sha1', 'abc'); ?>
:::
-
PHP - base64_decode()
:::spoiler
1
2
3
4<?php $str = 'VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw=='; echo base64_decode($str); // Output: This is an encoded string ?>
:::
Source Code
詳細的source code請參考HackMD筆記
Recon
這一題很複雜也需要很多的步驟
Exploit
- 先利用別人的腳本把PHP fuck轉換回原本的code
:::spoiler Script
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91using System; using System.IO; using System.Text.RegularExpressions; class Proj { // Fixes obfuscation pattern of the form ('['^'(').(')'^']') public static string RemoveParanthesisPattern(string data) { string pattern = @"\(('.'\^)+'.'\)"; // ('['^':') Regex regex = new Regex(pattern); MatchCollection matches = regex.Matches(data); int found = 0; foreach (Match match in matches) { // Console.WriteLine(match); // Console.WriteLine("----------------------------"); // Removing the first two characters (' string tmp = match.ToString().Remove(0, 1).Remove(0, 1); // Removing the last two characters ') tmp = tmp.Remove(tmp.Length - 2, 1).Remove(tmp.Length - 2, 1); tmp = tmp.Replace("'^'", "\x01"); int val = 0; foreach (var x in tmp.Split('\x01', StringSplitOptions.None)) { val ^= char.Parse(x); } data = data.Replace(match.ToString(), ((char)val).ToString()); found += 1; } Console.WriteLine($"RemoveParanthesisPattern: Total Matches Found: {found}"); return data; } // Fixes names of the form s.t.r.s.t.r --> strstr public static string FixDottedNames(string data, string pattern = "") { // XXX: Use this pattern if something breaks // string pattern = @"\((([a-z])\.)+[a-z]\)"; pattern = pattern == "" ? @"\((([a-z0-9_])\.)+[a-z]\)" : pattern; Regex regex = new Regex(pattern); MatchCollection matches = regex.Matches(data); int found = 0; foreach (Match match in matches) { string tmp = match.ToString(); if (tmp.StartsWith("(")) { tmp = tmp.Remove(tmp.Length - 1, 1).Remove(0, 1); } tmp = tmp.Replace(".", ""); data = data.Replace(match.ToString(), tmp); found += 1; } Console.WriteLine($"FixDottedNames: Total Matches Found: {found}"); return data; } static void Main(string[] args) { string filename = "obfuscated.php"; string fileData = File.ReadAllText(filename); fileData = RemoveParanthesisPattern(fileData); fileData = FixDottedNames(fileData); // Step3 fileData = fileData.Replace(@"''.abs(strstr('','.'))", "'0'"); // Step4 fileData = RemoveParanthesisPattern(fileData); fileData = FixDottedNames(fileData); fileData = FixDottedNames(fileData, @"(([a-z0-9A-Z_])\.)+[a-z0-9A-Z]"); fileData = fileData.Replace(".'0'.", "0"); File.WriteAllText("deobfuscated.php", "<?php\n" + fileData); } }
:::
:::spoiler Result
1
2
3
4
5
6
7
8
9
10
11
12<?php (in_array(count(get_included_files()),[1])?(strcmp(php_sapi_name(),cli)?printf(Use. .php.-.cli. .to. .run. .the. .challenge.!. ):printf(gzinflate(base64_decode(1dTBDYAgDAXQe6fgaC8O4DDdfwyhVGmhbaKe./.BfQfF8gAQFKz8aRh0JEJY0qIIenINTBEY3qNNVUAfuXzIGitJVqpiBa4yp2U8ZKtKmANzewbaqG2lrAGbNWslOvgD52lULNLfgY9ZiZtdxCsLJ3.+.Q./.2RVuOxji0jyl9aJfrZLJzxhgtS65TWS66wdr7fYzRFtvc./.wU9Wpn6BQGc))).define(F,readline(Flag.':'. )).(strcmp(strlen(constant(F)),41)?printf(Nope.!. ):(in_array(substr(constant(F),'0',5),[crew.{])?(strstr(strrev((crc32)(substr(constant(F),5,4))),7607349263)?(strnatcmp(A../.k,substr(constant(F),5,4)^substr(constant(F),9,4))?printf(Nope. .xor.!. ):srand(31337).define(D,openssl_decrypt(wCX3NcMho0BZO0SxG2kHxA.=.=,aes.-.128.-.cbc,substr(constant(F),'0',16),2,pack(L.*,rand(),rand(),rand(),rand()))).(in_array(array_sum([ctype_print(constant(D)),strpos(substr(constant(F),15,17),constant(D))]),[2])?(strcmp(base64_encode(hash(sha256,substr(constant(F),'0',32))^substr(constant(F),32)),BwdRVwUHBQVF)?printf(Nope.!. ):printf(Congratulations.','. .this. .is. .the. .right. .flag.!. )):printf(Nope.!. ))):printf(Nope.!. )):printf(Nope.!. )))):printf(Nope.!. )); ?>
::: 我另外把這坨東西弄的比較好讀一點 :::spoiler Beautiful Result
<?php (in_array(count(get_included_files()),[1])? ( strcmp(php_sapi_name(),cli)? printf(Use php-cli to run the challenge!): printf(gzinflate(base64_decode(1dTBDYAgDAXQe6fgaC8O4DDdfwyhVGmhbaKe/BfQfF8gAQFKz8aRh0JEJY0qIIenINTBEY3qNNVUAfuXzIGitJVqpiBa4yp2U8ZKtKmANzewbaqG2lrAGbNWslOvgD52lULNLfgY9ZiZtdxCsLJ3+Q/2RVuOxji0jyl9aJfrZLJzxhgtS65TWS66wdr7fYzRFtvc/wU9Wpn6BQGc))) define(F,readline(Flag':' )) ( strcmp(strlen(constant(F)),41)? printf(Nope!): ( in_array(substr(constant(F),'0',5),[crew{])? ( strstr(strrev((crc32)(substr(constant(F),5,4))),7607349263)? ( strnatcmp(A/k,substr(constant(F),5,4)^substr(constant(F),9,4))? printf(Nope xor!): srand(31337) define(D,openssl_decrypt(wCX3NcMho0BZO0SxG2kHxA==,aes-128-cbc,substr(constant(F),'0',16),2,pack(L*,rand(),rand(),rand(),rand()))) ( in_array(array_sum([ctype_print(constant(D)),strpos(substr(constant(F),15,17),constant(D))]),[2])? (strcmp(base64_encode(hash(sha256,substr(constant(F),'0',32))^substr(constant(F),32)),BwdRVwUHBQVF)? printf(Nope!): printf(Congratulations',' this is the right flag!)): printf(Nope!) ) ): printf(Nope!) ): printf(Nope!) ) ) ): printf(Nope!) ); ?>
:::
- 依照上面的background reference慢慢分析
- 首先第五行的printf不是很重要,他只是印出題目logo
:::spoiler
:::
- 第12行開始就是flag的驗證,前五個字元是==crew{==
- 第14行就是把我們輸入的flag從第五個字元開始算四個字元,先進行crc32的運算,然後在反轉string,然後看是不是等於
7607349263
,所以先reverse回去成正常的字串,然後用github上人家寫的crc32 unhash腳本1轉換有可能的字串,可以看到結果有幾種,不過因為這邊只有取4 bytes代表答案是==php_==$ ./psysh > strrev(7607349263) = "3629437067" $ python crc32.py reverse 3629437067 4 bytes: php_ {0x70, 0x68, 0x70, 0x5f} verification checksum: 0xd854d08b (OK) 6 bytes: Jhj4VW (OK) 6 bytes: KtdYLZ (OK) 6 bytes: Lmcgfq (OK) 6 bytes: Nlw5W4 (OK) 6 bytes: OpyXM9 (OK) 6 bytes: PNLKv5 (OK) 6 bytes: TJQJwV (OK) 6 bytes: WjZ94F (OK) 6 bytes: ZENzKX (OK) 6 bytes: apUQJ2 (OK) 6 bytes: bmOnaz (OK) 6 bytes: etHPKQ (OK) 6 bytes: tEbsLS (OK) 6 bytes: v4JPxF (OK) 6 bytes: yjv03M (OK) 6 bytes: yv9l2Y (OK)
- 第17行他先把剛剛得到的
php_
和後面的四個字元做xor,並比對A/k
,所以我們就把這幾個東西轉換成hex,再xor就好了1
2>>> bytes.fromhex('{:x}'.format(0x411b2F6B ^ 0x7068705F)).decode('utf-8') '1s_4'
- 第20行比較複雜,他先固定rand的seed,然後用openssl_decrypt解密一串密文,並和我們輸入的flag前16個字元做比較,但剛剛我們得到的flag只有到
crew{php_1s_4
共13個字元,代表我們要爆破剩下三個字元,所以我寫了一個php script和python script去擷取可能的結果 :::spoiler php script<?php $encryption = "wCX3NcMho0BZO0SxG2kHxA=="; $ciphering = "AES-128-CBC"; $decryption_key_ord = "crew{php_1s_4"; $options = 2; $str=array('0','1','2','3','4','5','6','7','8','9','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','_'); for ($x = 0; $x <= 36; $x++) { for ($y = 0; $y <= 36; $y++) { for ($z = 0; $z <= 36; $z++) { srand(31337); $decryption_key = $decryption_key_ord.$str[$x].$str[$y].$str[$z]; // echo $decryption_key; $decryption_iv = pack("L*",rand(),rand(),rand(),rand()); $decryption=openssl_decrypt ($encryption, $ciphering, $decryption_key, $options, $decryption_iv); echo $str[$x].$str[$y].$str[$z]. " is " . $decryption. "\n"; } } } ?>
:::
:::spoiler python script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import string candidate = string.ascii_lowercase + string.digits + "_ " def check_characters(string): for char in string: if char not in candidate: return False return True f = open("./result.txt", "rb").read().splitlines()#D:/Download/Trash for i in range(len(f)): try: tmp = f[i].decode() if check_characters(tmp): print(f[i].decode()) except: pass
:::
1
2
3
4$ php exp.php > result.txt $ python exp.py ... _l4 is ngu4ge_0f_m4g1c_
在print出來的東西當中有一個特別長,那就是我們要找的三個字元和解密出來的東西,所以目前為止的flag是==crew{php_1s_4l4ngu4ge_0f_m4g1c==
- 最後就是第22行的部分,他就是把
crew{php_1s_4_l4ngu4ge_0f_m4g1c_
進行SHA256和最後剩下的部分做XOR,再把結果進行base64,和BwdRVwUHBQVF
比較(不知道為甚麼一樣的操作用python會失敗,我想應該是因為php有特別的操作?!)1
2
3$ ./psysh > echo hash('sha256',"crew{php_1s_4_l4ngu4ge_0f_m4g1c_") ^ base64_decode("BwdRVwUHBQVF") 5b0e7b6a}
- 首先第五行的printf不是很重要,他只是印出題目logo
:::spoiler
Flag: crew{php_1s_4_l4ngu4ge_0f_m4g1c_5b0e7b6a}