This weekend, I played RWCTF with WreckTheLine and got 7th place :)
scoreboard
I didn’t intend to play initially, because I always thought RWCTF challenges
are too convoluted for me to even try. Also IB deadlines exist so I haven’t
been playing CTFs much :(
Anyway in the end I changed my mind and spent most of my time on a single
challenge.
After seeing Java I absolutely needed to solve this challenge, cos I haven’t
actually solved a Java web chall before. I opened the .jar in jadx and took a
look.
My goal is just to login as admin. The way their login is implemented is such
that it queries the user’s password and compares it server-side, so I can’t
simply do the 1=1 exploit. Usually I would do ' union select 1, 'asdf but
both union and select are blocked. I tried putting null bytes and surprisingly
got an error (insufficient data left in message) from the DB connector, so I
thought I was going somewhere (apparently not).
...un%00ion se%00lect 1, 'asdf
(does not work)
Other stuff I tried included intersect select (still blocked), coalesce
error-based SQLi and a bunch of other stuff I forgot. I even tried playing with
the normalization to see if any non-standard characters can get past the
blacklist when uppercased, but evaluate correctly in the DB (did not work).
At that point I was kinda ded so I went to sleep. I dreamt of Java types and
similarly fun stuff.
The next day I realized there’s no way I can get past using any SQL commands. I
then focused on trying to manipulate the username comparison somehow. (At that
point, catalin had suggested we can create a new user with id=1 to fake an
admin account, since its not primary key field).
After doing lots of random stuff in the Postgres console, I tried casting the
passwd to ::int and got an error with the password in it! Since the login page
returns the error message, I can use this to leak the password!
The backend parses the statement using some SQL parser library and restricts
the expression to a few measly types. It’s slightly complex so I didn’t try to
conform my payload to match it.
I noticed that if the (2) parsers crash, the backend defaults to a very weak
filtering function:
publicstaticbooleanfilter(String sql) { if (StringUtil.matches(sql, "^[a-zA-Z0-9_]*$") || sql.contains(" USER_DEFINE ")) { returntrue; } if (sql.startsWith("SELECT") && sql.contains("VIEW")) { returntrue; } for (String whitePrefix : getWhitePrefix().stream()) { if (sql.startsWith(whitePrefix)) { returntrue; } } returnfalse; }
This function is very easy to bypass! I love it!
So I just need a query that crashes the parser but doesn’t crash in Postgres.
After searching the Github issues for the 1st parser I found
this. Basically since
the author is too lazy the parser crashes on this valid Postgres syntax:
selectdouble precision'1'
(but since do is blocked by blacklist I need to find some other type to cast to)
So in my payload, I can add this to bypass the parser, and add user_define to pass filter function.
While I was trying to get file write in the SQLi, adragos found path traversal
+ undocumented LFI to FTP in the /notify endpoint.
?fname=..%5c..%5c..%5c/endpoint/test.txt%23
(\ will fail in catalina reverse proxy, and # strips out the .html
extension (not necessary for final payload though))
The additional ..\ is apparently parsed such that the hostname of
file://host/path can be controlled. This is super cool because we can
escalate to RFI, which bypassed the need for Postgres file write.
(from here on adragos already solved everything, but since I had to solve it I
tried on my own anyway)
So I hosted an FTP server to load the attacker template.
Part 3: SSTI
Thymeleaf 13.0.2 is quite annoying cos it added a lot of restrictions to SSTI
in “unsafe” context, by preventing instantiation of several common key classes.
This was also my first time doing Thymeleaf SSTI (or Java SSTI in general) so I
was quite lost. After adragos solved the challenge I peeked at his payload then
tried to make my own.
The trick is to look for gadgets in the other libraries loaded in the app, that
allow you to instantiate classes with calls such as
constructor.newInstance(args) etc.
T(class) is quite useful for instantiating classes from qualified name, but this
is blocked by Thymeleaf restrictions, so I can’t just instantiate
java.lang.Runtime.
"".getClass().forName(class) also can be used to create classes and isn’t
blocked by Thymeleaf I think? But the class instantiated from here is useless
cos I can’t call any methods on it for some reason? (need to research more)
Anyway, org.postgresql.util.ObjectFactory has the .instantiate method which
can be used to create FileSystemXmlApplicationContext with correct
arguments. This class will parse external XML using templating processing,
which we can again host on attacker server. This time, our SSTI will have 0
restrictions :)