[2020-angstromCTF] web - A peculiar query write-up
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}
from http://eine.tistory.com/211 by ccl(A) rewrite - 2020-03-19 10:54:05
댓글
댓글 쓰기