English write-up
UI seems like below.
If you click the 'the source' link, you can get back-end source code.
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");
const client = new Client({
user: process.env.DBUSER,
host: process.env.DBHOST,
database: process.env.DBNAME,
password: process.env.DBPASS,
port: process.env.DBPORT
});
async function query(q) {
const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
return ret;
}
app.set("view engine", "ejs");
app.use(express.static("public"));
app.get("/src", (req, res) => {
res.sendFile(path.join(__dirname, "index.js"));
});
app.get("/", async (req, res) => {
if (req.query.q) {
try {
let q = req.query.q;
// no more table dropping for you
let censored = false;
for (let i = 0; i < q.length; i ++) {
if (censored || "'-\".".split``.some(v => v == q[i])) {
censored = true;
q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
}
}
q = q.substring(0, 80);
const result = await query(q);
res.render("home", {results: result.rows, err: ""});
} catch (err) {
console.log(err);
res.status(500);
res.render("home", {results: [], err: "aight wtf stop breaking things"});
}
} else {
res.render("home", {results: [], err: ""});
}
});
app.listen(port, function() {
client.connect();
console.log("App listening on port " + port);
});
Code is written in javascript, so the server platform is nodejs.
There is simple sql execution function, it filters some special chars.
It filters single-quotor, double-quotor, dash and point.
If one of bad-char appears, the all remainders will be substituted to '*' char.
I thought hard how can I bypass the filter, a solution is javascript type confusion.
The server source code assumes that 'req.query.q' data type is string, if you send url querystring like "q[]=value&q[]=another", on the server side the value is ["value", "another"] which is javascript array type.
Then q[i] is no longer single character but string, so we can bypass the filtering.
The expression ("'or 1=1-- " == "'") is evaluated as false.
Moreover, javascript array object also have method "slice" like string. The function slightly differ.
If you try to + operation between javascript array and string, the result is string. The array is treated like string.
Then after the expression `slice(0,i) + "*" + slice(i+1, q.length)`, q is now string, we can do the q.substring method below without exception.
I coded query string exploit payload builder in javascript.
function go(payload) {
var ret = '?q[]=' + encodeURIComponent(payload);
for (var i =1; i < payload.length; i++) {
ret += `&q[]`;
}
ret += `&q[]='`
return ret;
}
For the test, I wrote a query to get the current database name in pg-sql.
It works well!
I tried to get the column name with information_schema table, but q.substring(0, 80) limit our query length to 80, I did another method.
With some functions in pg-sql, we can get the data in 'Criminals' table in json serialized format.
Gotcha!, We've got the flag
actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}
한글 풀이
UI는 아래와 같이 생겼다.
우측에 the source라는 것을 클릭하면 백엔드 소스도 제공을 해준다.
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const { Pool, Client } = require("pg");
const port = process.env.PORT || 9090;
const path = require("path");
const client = new Client({
user: process.env.DBUSER,
host: process.env.DBHOST,
database: process.env.DBNAME,
password: process.env.DBPASS,
port: process.env.DBPORT
});
async function query(q) {
const ret = await client.query(`SELECT name FROM Criminals WHERE name ILIKE '${q}%';`);
return ret;
}
app.set("view engine", "ejs");
app.use(express.static("public"));
app.get("/src", (req, res) => {
res.sendFile(path.join(__dirname, "index.js"));
});
app.get("/", async (req, res) => {
if (req.query.q) {
try {
let q = req.query.q;
// no more table dropping for you
let censored = false;
for (let i = 0; i < q.length; i ++) {
if (censored || "'-\".".split``.some(v => v == q[i])) {
censored = true;
q = q.slice(0, i) + "*" + q.slice(i + 1, q.length);
}
}
q = q.substring(0, 80);
const result = await query(q);
res.render("home", {results: result.rows, err: ""});
} catch (err) {
console.log(err);
res.status(500);
res.render("home", {results: [], err: "aight wtf stop breaking things"});
}
} else {
res.render("home", {results: [], err: ""});
}
});
app.listen(port, function() {
client.connect();
console.log("App listening on port " + port);
});
코드를 보니 nodejs로 백엔드를 작성했다.
간단하게 sql 쿼리를 실행시킬 수 있도록 되어있고, 일부 특수문자들을 필터링하는 것을 알 수 있다.
일단 싱글쿼터를 필터링을 해서, 해당 bad character가 나타나면 나머지 모든 글자들을 *로 바꿔버리는 방식이다.
이 sql 쿼리 필터링을 어떻게 우회할까 고민을 많이 했는데, 정답은 javascript type confusion이었다.
서버코드는 req.query.q가 string일 것을 가정하고 코드가 짜져있는데, url 쿼리스트링에 q[]=value&q[]=another 와 같은 방식으로 전송하면 서버에는 ["value", "another"]과 같은 javascript array형태로 전송이 되게 된다.
그러면 some(v => v == q[i])라는 필터링에서도 ["'or 1=1-- ", "garbage"] 이런 값이 전송이 되는 경우 filtering이 제대로 되지 않게 된다. "'or 1=1-- " == "'" 는 당연 false가 나오기 때문.
게다가 Javascript array는 slice라는 함수를 동일하게 가지고 있다.
그리고 문자열과 배열간 + 연산을 하게 되면, 배열을 문자열처럼 바뀌어서 concat연산이 되고 그 결과는 문자열이 되어서 아래의 q.substring 함수도 정상적으로 실행을 하게 된다.
이러한 조건에 맞는 query string payload를 만들어주는 js 코드를 작성해서 요청을 보내보았다.
function go(payload) {
var ret = '?q[]=' + encodeURIComponent(payload);
for (var i =1; i < payload.length; i++) {
ret += `&q[]`;
}
ret += `&q[]='`
return ret;
}
일단 테스트 겸 database name을 알아내는 쿼리를 작성해보았다.
결과가 잘 나온다.
information_schema로 column 명을 알아내려고 했는데, q.substring(0, 80)의 쿼리 길이 제한때문에 잘 안되서 다른 방법을 사용해보기로 했다.
이제 Criminals 테이블의 값을 json형태로 serialize해서 다 빼오는 쿼리를 작성해서 날려보자.
flag를 얻었다.
actf{qu3r7_s7r1ng5_4r3_0u7_70_g37_y0u}
'해킹 & 보안 > CTF Write-ups' 카테고리의 다른 글
[Defenit CTF 2020] babyjs write-up (0) | 2020.06.07 |
---|---|
[Defcon ctf qual 2019] shitorrent write-up (0) | 2020.03.29 |
[Trust CTF 2020] 127% Machine / ezrc / grade program write-ups (0) | 2020.02.28 |
[NeverLAN CTF 2020] Web - SQL Breaker 1 / SQL Breaker 2 / DasPrime (0) | 2020.02.15 |
[Insomni'hack teaser 2020] web - Low Deep write-up (0) | 2020.01.20 |