Author:颖奇L’Amore
Blog:www.gem-love.com
This weekend I played N1CTF with team r3kapig and finally we got the 2nd🥈 place. Thanks to all my teammates for their hard work, and also to the Nu1L team for holding the wonderful & awesome game.
Btw, writeup for all challenges we solved will be published here, check it out! I will write some of them in detail
web-signin▸
This challenge is the easiest web challenge, but it is very interesting especially for those who are new in CTF. It gives the source code It is easy to know that it is an unserialize challenge, and we can ctrl the serialize string totally. It means that you can create Object with any attributes’ value
Then let’s find the pop chain. it is easy to know that the purpose is to call flag->getflag()
method by automatically calling __destruct()
magic method, and $check
attribute must be eq to a censored string "key****************"
. So now what we need to do is getting this secret string. Fortunately there is a ip
class with a __toString()
magic method. If you treat an Object as a string, it will automatically call the object’s __toString()
method, and what __toString
returns will be used. ip->__toString()
will execute a mysql insert
query, and one of the values is $this->waf($_SERVER['HTTP_X_FORWARDED_FOR'])
, we can easily ctrl X-Forwarded-For
header so it hints that it is a SQL inject challenge. But we don’t know what waf()
method do but it must filter some sql keywords otherwise it would be a really really really easy challenge(in fact, even it has waf it is still not difficult). By fuzzing, we suppose that waf()
method may look like this:
function waf($info){ |
according to the pattern, we cannot use Time-based Blind SQLi. But insert
query returns nothing useful, so now we must find a way to get the sub select query result. The first time I wanted to use mysql load_file()
to send a http request and use dnslog to receive the result but failed. Then I noticed that ip->__toString()
will return mysqli_error($con)
, so maybe I can use Error-based SQLi. The exploit chain looks like:
-
flag
class’s member variable$ip
is anip
object instance - once
unserialize()
create theflag
instance,__wakeup()
method called - in
__wakeup()
,stristr()
function treat$this-ip
as a string, and now$this->ip
isip
object instance, so it calledip->_toString()
method -
__toString
do an sql query with error-based sqli payload(I’ll show you my payload later) in it because we controlled the XFF header. if the blind sqli expression in error-based sqli payload returns true,__toString
will return mysql error message, otherwise it returns “your ip looks ok!” - since error-based sqli will return the sub select result, just decorate your payload to make sure it will return “n1ctf” in the hole error message when blind sqli returns true in sub select query(If you dont understand, dont worry, u will understand it after reading the payload). Then
__wakeup
will set$this->ip
to “welcome to n1ctf2020”. If blind sqli returns false,$this->ip
will be “noip” - then
flag
automatically called__destruct()
because it has nothing else to do, the object needs to be destroyed.echo $this->getflag()
willecho $this->ip
because we still dont know the key now. - So, we have an error-based sqli payload will a sub select query in it. And the sub select query is a blind sqli payload, what this sub query returns will be up to a boolean expression. This boolean expression will finally control what is echoed when
__destruct()
. - According to the 2 different echo result, we can do Boolean-based SQL!
now I will show you my sqli payload:
'(select ip from n1ip where _updatexml(1,concat('~',(select if(ascii(substring((select database()),1,1))=100,'n1ctf','r3kapig')),'~'),3))'_ |
The italics are updatexml()
error-based sqli, the blue part are the sub query I mentioned above, the pink part are what you want to dump, and the underline part is the boolean expression I also mentioned above. I think it’s time to write exp
|
#!/usr/bin/env python3 |
What need to be noticed is that you can’t dump key by select key from n1key
but select `key` from n1key
. because key
is a keyword of mysql and `key`
means a column named key. After dumping the key, we can get flag by unserialize again
|
flag: n1ctf{you_g0t_1t_hack_for_fun}
Misc-filter▸
I didnt solve this challenge because of several reasons, for example, my computer is out of power and it wouldn’t help us get champion. But this challege is quite interesting so that I decided to record our idea.
|
also provided Dockerfile:
FROM ubuntu:18.04 |
You can give any filters you want, you can also give more than one filters divided by /
, then it will read file by using php://filter
stream and push the result to an array. After that shuffle()
function shuffles (randomizes the order of the elements in) an array. Finally write the array to a file and include the file. Obviously we need to RCE by using require_once $source_file
. It is interesting that flag is on the web root directory and it named itself as it content(cat /tmp/flag > /var/www/html/`cat /tmp/flag`
). So the first code I wanna write into $source_file
to include is <?=`ls`;
Then what I need to do is get these chars(or substring of <?=`ls`;
), I write a simple fuzzer to get them.
#!/usr/bin/env python3 |
To be honest, this fuzzer script has a lot to improve on. For example, it can fuzz more than one char. If it find some filters that can get <?=`
, the challenge will become very simple. It’s easy to improve but I didn’t because we have high performance servers and two of my teammates help me run the script(main reason is I’m lazy). Finally we fuzzed out all these 8 characters. since there are two `
, it just need to send about 7! = 5040 requests to let shuffle()
get those 8 chars in the right order. for example, these filters can get `
string.strip_tagsconvert.base64-decodestring.strip_tagsconvert.base64-encodeconvert.base64-encodeconvert.base64-decodestring.strip_tagsstring.rot13convert.base64-encodestring.rot13convert.base64-encodestring.rot13string.strip_tagsstring.strip_tagsstring.strip_tagsconvert.base64-decodestring.rot13convert.base64-decodestring.rot13string.rot13 |
BUT!!! when I prepare to get flag, I found that local environment is different from the challenge!
It may be challenge author’s fault. Maybe he changed the php binary file or some other reason. We can still solve this challenge by download /usr/bin/php firstly and locally fuzz again, but I gave up because it was unnecessary. Solving this challenge wouldn’t let us get the first prize, but we have found the right way to solve the challenge, and that is enough. Challenge author’s fuzzer script here This challenge also has an unintended solution, you can check out Super Guesser‘s Writeup
web-zabbix_fun▸
build on local by using docker-compose up -d --build
version: '2' |
This challenge is quite easy. After discussing with author, the intended solution is get a shell of zabbix server, but in fact it is totally unnecessary. What I did firstly is read zabbix document for more than 1h. First, login with Admin/zabbix
Then configure the host(zabbix agent) and make sure zbx
is green
by default it is red because of the wrong config
what we must need to configure the host is agent’s ip and port. I got them from docker
configure the host and make ZBX green(green means available)
ZABBIX is used to monitor the server. noticed that it is monitoring whether the /etc/passwd of the agent is modified by vfs.file.cksum
, which indicates that zabbix has the right to read files on agent.
and what is vfs.file.cksum
? it is an item
then I thought there must be an item to read file content, and it’s true
now let get flag by using this item. And I got the 2nd blood of this challenge.
Easy but interesting! In fact, at the first time I configured the item but I don’t know how to Get value. Then I changed to try to RCE. What I did is configure a trigger -> trigger an action -> action execute command. But I failed because by default the agent don’t support to execute command.