기본 콘텐츠로 건너뛰기

[2020-angstromCTF] web - A peculiar query write-up

[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

댓글

이 블로그의 인기 게시물

[실습]NodeJS + EXPRESS + MySQL 을 이용한 게시판 만들기 3(MVC)

[실습]NodeJS + EXPRESS + MySQL 을 이용한 게시판 만들기 3(MVC) 실습2 이후 나머지 부분들 까지 라우터 모두 작성함 app/routes/posts.js app/controllers/postsController.js 현재까지의 소스 app/models/postsModel.js 현재까지의 소스 from http://thisblogbusy.tistory.com/139 by ccl(A) rewrite - 2020-03-15 09:20:05

AWS instance로 Nodejs 구현하기

AWS instance로 Nodejs 구현하기 서버와 데이터베이스 관리 차원에서 효율적으로 관리하기 위해선 로컬보다는 서버를 호스팅해서 하는 것이 좋다. 우리는 Nodejs를 구동하기 위해 AWS에서 인스턴스를 할당받을 계획이다. 인스턴스의 pem키를 발급받아 nodejs와 npm까지는 설치를 완료한 상태이다. $ sudo npm install -g express 다음의 명령어를 입력하면 글로벌 옵션으로 어느 path에서든 express를 사용할 수 있게 설치한다. 다음과 같이 실행이 된다면 성공이다. 이후 Express generator를 설치한다. $ sudo npm install -g express-generator@4 버전은 4.x이며 이 역시 글로벌 옵션으로 설치해 준다. 이제 Node monitoring을 위해 nodemon을 설치해 준다. $ sudo npm install -g nodemon 모든 설치가 끝났다. 이제 nodejs를 실행시킬 프로젝트용 directory를 만든다. 이렇게 만들어 주고 express를 실행시키면 된다. $ express -e 다음과 같은 결과가 나오면 된다. 이제 node package를 설치하는 명령어를 입력하자. $ sudo npm install 이제 vi를 통해 포트번호를 정의해보자. app.set의 마지막에 한줄을 추가하면 된다. app.set('port', process.env.PORT || 9000); 이로써 우리는 9000번 포트를 사용하게 되었다. 또한 마지막줄에 서버를 생성하기 위한 코드를 작성하자. module.exports = app; var server = app.listen(app.get('port'), function() { console.log('Express server listening on port ' + server.address().port); }); 이